循序渐进的示例和清晰的叙述使这本书成为 Unity 的首选书籍!
Step-by-step examples and clear prose make this the go-to book for Unity!
—Victor M. Perez, Software developer
您需要了解的有关 Unity 的所有内容都包含在一个资源中。
Everything you need to know about Unity in one single resource.
—Dan Kacenjar, Cornerstone Software
Start creating your own game prototypes in no time.
—David Torrubia Iñigo,Fintonic
—David Torrubia Iñigo, Fintonic
The text is clear and concise, and the examples are outstanding.
—Dan Kacenjar, Sr., Wolters Kluwer
All the roadblocks evaporated, and I took my game from concept to build in short order.
Joe Hocking wastes none of your time and gets you coding fast.
—Jesse Schell, author of The Art of Game Design
I’ve wanted to program in Unity for a long time, and this book has given me the confidence to do so.
如需在线了解这些书籍和其他 Manning 书籍的信息以及订购它们,请访问www.manning.com。出版商为这些书籍提供批量订购折扣。
For online information and ordering of these and other Manning books, please visit www.manning.com. The publisher offers discounts on these books when ordered in quantity.
如需了解更多信息,请联系
For more information, please contact
特约销售部
Special Sales Department
曼宁出版公司
Manning Publications Co.
鲍德温路 20 号
20 Baldwin Road
邮政信箱 761
PO Box 761
纽约州谢尔特岛 11964
Shelter Island, NY 11964
邮箱: orders@manning.com
Email: orders@manning.com
©2022 曼宁出版公司版权所有。保留所有权利。
©2022 by Manning Publications Co. All rights reserved.
未经出版商事先书面许可,不得以任何形式或通过电子、机械、影印或其他方式复制、存储在检索系统中或传播本出版物的任何部分。
No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form or by means electronic, mechanical, photocopying, or otherwise, without prior written permission of the publisher.
制造商和销售商用来区分其产品的许多名称均已声明为商标。这些名称出现在书中,并且 Manning Publications 知道商标声明,这些名称均以首字母大写或全部大写印刷。
Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in the book, and Manning Publications was aware of a trademark claim, the designations have been printed in initial caps or all caps.
♾认识到保存已写内容的重要性,Manning 的政策是将我们出版的书籍印刷在无酸纸上,并为此尽最大努力。同时认识到我们有责任保护地球资源,Manning 书籍印刷在至少 15% 的回收纸上,并且在加工过程中不使用元素氯。
♾ Recognizing the importance of preserving what has been written, it is Manning’s policy to have the books we publish printed on acid-free paper, and we exert our best efforts to that end. Recognizing also our responsibility to conserve the resources of our planet, Manning books are printed on paper that is at least 15 percent recycled and processed without the use of elemental chlorine.
|
|
曼宁出版公司 Manning Publications Co. 20 鲍德温路技术 20 Baldwin Road Technical 邮政信箱 761 PO Box 761 纽约州谢尔特岛 11964 Shelter Island, NY 11964 |
|
开发编辑器: Development editor: |
贝基·惠特尼 Becky Whitney |
|
评论编辑: Review editor: |
米哈埃拉·巴蒂尼奇 Mihaela Batinić |
|
制作编辑: Production editor: |
迪尔德丽·S·希亚姆 Deirdre S. Hiam |
|
文字编辑: Copy editor: |
莎朗·威尔基 Sharon Wilkey |
|
校对: Proofreader: |
杰森·埃弗里特 Jason Everett |
|
排字员: Typesetter: |
戈登·萨利诺维奇 Gordan Salinović |
|
封面设计师: Cover designer: |
玛丽亚·都铎 Marija Tudor |
国际标准书号:9781617299339
ISBN: 9781617299339
Unity’s strengths and advantages
Example games built with Unity
Scene view, Game view, and the Toolbar
The Hierarchy view and the Inspector panel
1.3 Getting up and running with Unity programming
Running code in Unity: Script components
Using Visual Studio, the included IDE
Printing to the console: Hello World!
2 Building a demo that puts you in 3D space
Understanding 3D coordinate space
2.2 Begin the project: Place objects in the scene
The scenery: Floor, outer walls, and inner walls
The player’s collider and viewpoint
2.3 Make things move: A script that applies transforms
Visualizing how movement is programmed
Writing code to implement the diagram
Understanding local vs. global coordinate space
2.4 Script component for looking around: MouseLook
Horizontal rotation that tracks mouse movement
Horizontal and vertical rotation at the same time
2.5 Keyboard input component: First-person controls
Setting a rate of movement independent of the computer’s speed
Moving the CharacterController for collision detection
Adjusting components for walking instead of flying
3 Adding enemies and projectiles to the 3D game
Using the ScreenPointToRay command for shooting
Adding visual indicators for aiming and hits
3.2 Scripting reactive targets
Alerting the target that it was hit
Diagramming how basic AI works
“Seeing” obstacles with a raycast
Tracking the character’s state
Instantiating from an invisible SceneController
3.5 Shooting by instantiating objects
Creating the projectile prefab
Shooting the projectile and colliding with a target
4 Developing graphics for your game
4.2 Building basic 3D scenery: Whiteboxing
Drawing a floor plan for the level
Laying out primitives according to the plan
4.3 Texturing the scene with 2D images
4.4 Generating sky visuals by using texture images
Creating a new skybox material
4.5 Working with custom 3D models
Exporting and importing the model
4.6 Creating effects by using particle systems
Adjusting parameters on the default effect
Applying a new texture for fire
Attaching particle effects to 3D objects
5 Building a Memory game using Unity’s 2D functionality
5.1 Setting up everything for 2D graphics
Displaying 2D images (aka sprites)
Switching the camera to 2D mode
5.2 Building a card object and making it react to clicks
Building the object out of sprites
5.3 Displaying the various card images
Loading images programmatically
Setting the image from an invisible SceneController
5.4 Making and scoring matches
Storing and comparing revealed cards
使用 SendMessage 对 UIButton 组件进行编程
Programming a UIButton component by using SendMessage
从 SceneController 调用 LoadScene
Calling LoadScene from SceneController
6 Creating a basic 2D platformer
6.2 Moving the player left and right
6.3 Playing the sprite’s animation
Explaining the Mecanim animation system
Triggering animations from code
6.4 Adding the ability to jump
6.5 Additional features for a platform game
Unusual floors: Slopes and one-way platforms
7.1 Before you start writing code . . .
Immediate mode GUI or advanced 2D interface?
7.2 Setting up the GUI display
Creating a canvas for the interface
Buttons, images, and text labels
Controlling the position of UI elements
7.3 Programming interactivity in the UI
Programming an invisible UIController
Setting values using sliders and input fields
7.4 Updating the game by responding to events
Broadcasting and listening for events from the scene
Broadcasting and listening for events from the HUD
8 Creating a third-person 3D game: Player movement and animation
8.1 Adjusting the camera view for third-person
Importing a character to look at
Orbiting the camera around the player character
8.2 Programming camera-relative movement controls
Rotating the character to face movement direction
Moving forward in that direction
8.3 Implementing the jump action
Applying vertical speed and acceleration
Modifying the ground detection to handle edges and slopes
8.4 Setting up animations on the player character
Defining animation clips in the imported model
Creating the animator controller for these animations
Writing code that operates the animator
9 Adding interactive devices and items within the game
9.1 Creating doors and other devices
Doors that open and close on a keypress
Checking distance and facing before opening the door
Operating a color-changing monitor
9.2 Interacting with objects by bumping into them
Colliding with physics-enabled obstacles
Operating the door with a trigger object
Collecting items scattered around the level
9.3 Managing inventory data and game state
Setting up player and inventory managers
Storing inventory in a collection object: List vs. Dictionary
9.4 Inventory UI for using and equipping items
Displaying inventory items in the UI
Equipping a key to use on locked doors
Restoring the player’s health by consuming health packs
10 Connecting your game to the internet
10.1 Creating an outdoor scene
Generating sky visuals by using a skybox
Setting up an atmosphere that’s controlled by code
10.2 Downloading weather data from an internet service
Requesting HTTP data using coroutines
Affecting the scene based on weather data
10.3 Adding a networked billboard
Loading images from the internet
Displaying images on the billboard
Caching the downloaded image for reuse
10.4 Posting data to a web server
Tracking current weather: Sending post requests
11 Playing audio: Sound effects and music
Explaining what’s involved: Audio clip vs. source vs. listener
Triggering sound effects from code
11.3 Using the audio control interface
Setting up the central AudioManager
Controlling music volume separately
12 Putting the parts together into a complete game
12.1 Building an action RPG by repurposing projects
Assembling assets and code from multiple projects
Programming point-and-click controls: Movement and devices
Replacing the old GUI with a new interface
12.2 Developing the overarching game structure
Controlling mission flow and multiple levels
Completing a level by reaching the exit
Losing the level when caught by enemies
12.3 Handling the player’s progression through the game
Saving and loading the player’s progress
Beating the game by completing three levels
13 Deploying your game to players’ devices
13.1 从桌面构建开始:Windows、Mac 和 Linux
13.1 Start by building for the desktop: Windows, Mac, and Linux
Adjusting player settings: Setting the game’s name and icon
Platform-dependent compilation
Building the game embedded in a web page
Communicating with JavaScript in the browser
13.3 Building for mobile: iOS and Android
13.4 Developing XR (both VR and AR)
Supporting virtual reality headsets
AR Foundation for mobile Augmented Reality
appendix A Scene navigation and keyboard shortcuts
appendix B External tools used alongside Unity
appendix C Modeling a bench in Blender
我从 1982 年开始编写游戏。这并不容易。当时我们没有互联网。资源仅限于少数几本大多很糟糕的书籍和杂志,它们提供的代码片段既迷人又令人困惑,至于游戏引擎——根本没有!编写游戏代码是一场艰苦的战斗。
I started programming games in 1982. It wasn’t easy. We had no internet. Resources were limited to a handful of mostly terrible books and magazines that offered fascinating but confusing code fragments, and as for game engines—well, there weren’t any! Coding games was a massive uphill battle.
读者,我多么羡慕你,能拥有这本书的力量。Unity 引擎为向如此多的人开放游戏编程做出了巨大贡献。Unity 成功地实现了极好的平衡,它是一款功能强大、专业的游戏引擎,对于刚入门的人来说,它仍然价格合理、易于上手。
How I envy you, reader, holding the power of this book in your hands. The Unity engine has done so much to open game programming up to so many people. Unity has managed to strike an excellent balance by being a powerful, professional game engine that’s still affordable and approachable for someone just getting started.
平易近人,即在正确的指导下。我曾经在一个由魔术师经营的马戏团里待过一段时间。他很好心地收留了我,并帮助我成为一名优秀的表演者。“当你站在舞台上时,”他说,“你就做出了承诺。这个承诺就是‘我不会浪费你的时间’。”
Approachable, that is, with the right guidance. I once spent time in a circus troupe run by a magician. He was kind enough to take me in and helped guide me toward becoming a good performer. “When you stand on a stage,” he pronounced, “you make a promise. And that promise is ‘I will not waste your time.’”
我最喜欢《Unity in Action》的“行动”部分。Joe Hocking 不会浪费你的时间,让你快速编写代码 — — 并且不只是无意义的代码,而是你可以理解和构建的有趣代码,因为他知道你不只是想读他的书,你不只是想编写他的例子 — — 你想编写自己的游戏。
What I love most about Unity in Action is the “action” part. Joe Hocking wastes none of your time and gets you coding fast—and not just nonsense code, but interesting code that you can understand and build from, because he knows you don’t just want to read his book, and you don’t just want to program his examples—you want to be coding your own game.
在他的指导下,你将能够比预期更快地做到这一点。跟随乔的脚步,但当你准备好时,不要羞于偏离他的路径并独自突破。跳到你最感兴趣的部分——尝试实验,大胆而勇敢!如果你太迷茫,你可以随时返回文本。
And with his guidance, you’ll be able to do that sooner than you might expect. Follow Joe’s steps, but when you feel ready, don’t be shy about diverging from his path and breaking out on your own. Skip to what interests you most—try experiments, be bold and brave! You can always return to the text if you get too lost.
但是,我们不要在前言中拖拖拉拉——整个游戏开发的未来都在焦急地等待着你开始!在你的日历上标记这一天,因为今天是一切改变的日子。它将永远被铭记为你开始制作游戏的日子。
But let’s not dally in this foreword—the entire future of game development is impatiently waiting for you to begin! Mark this day on your calendar, for today is the day that everything changed. It will be forever remembered as the day you started making games.
—Jesse Schell,Schell Games 首席执行官,《游戏设计的艺术》作者
—Jesse Schell, CEO of Schell Games, Author of The Art of Game Design
我从事游戏编程已有一段时间了,但最近才开始使用 Unity。我刚开始开发游戏时 Unity 还不存在;第一个版本于 2005 年发布。从一开始,它就被看作是一款很有前途的游戏开发工具,但直到几个版本之后才开始发挥作用。特别是,iOS 和 Android 等平台(统称为移动平台)直到后来才出现,这些平台对 Unity 日益突出的影响力起到了重要作用。
I’ve been programming games for quite some time, but started using Unity only relatively recently. Unity didn’t exist when I first started developing games; the first version was released in 2005. Right from the start, it had a lot of promise as a game development tool, but it didn’t come into its own until several versions later. In particular, platforms like iOS and Android (collectively referred to as mobile) didn’t emerge until later, and those platforms factor heavily into Unity’s growing prominence.
最初,我把 Unity 视为一种新奇事物,一种值得关注但实际上并不使用的有趣开发工具。在那段时间里,我为台式电脑和网站编写游戏,并为各种客户做项目。我使用 Blitz3D 和 Adobe Flash 等工具,这些工具编程起来很棒,但在很多方面存在限制。随着这些工具开始显露出它们的年龄,我一直在寻找更好的游戏开发方法。
Initially, I viewed Unity as a curiosity, an interesting development tool to keep an eye on but not actually use. During that time, I was programming games for both desktop computers and websites and doing projects for a range of clients. I was using tools like Blitz3D and Adobe Flash, which were great to program in but were limiting in a lot of ways. As those tools started to show their age, I kept looking for better ways to develop games.
我从 Unity 3 开始试用,之后在 Synapse Games 的开发工作中完全转向使用它。起初,我在 Synapse 从事网页游戏工作,但最终我们转向了移动游戏。然后我们又回到了原点,因为 Unity 使我们能够将游戏部署到网页和移动端,而且只需一个代码库!
I started experimenting with Unity around version 3 and then completely switched to it for my development work at Synapse Games. At first, I worked for Synapse on web games, but we eventually moved over to mobile games. And then we came full circle because Unity enabled us to deploy to the web in addition to mobile, all from one codebase!
我一直认为分享知识很重要,并且已经教授游戏开发好几年了。我这样做的很大一部分原因是我的许多导师和老师树立的榜样。(顺便说一句,你甚至可能听说过我的一位老师,因为他是一位非常鼓舞人心的人:兰迪·波许在 2008 年去世前不久发表了“最后的演讲”。)我曾在几所学校教过课,一直想写一本关于游戏开发的书。
I’ve always seen sharing knowledge as important and have taught game development for several years. A large part of why I do this is the example set by my many mentors and teachers. (Incidentally, you may even have heard of one of my teachers because he was such an inspiring person: Randy Pausch delivered “The Last Lecture” shortly before he passed away in 2008.) I’ve taught classes at several schools and have always wanted to write a book about game development.
从很多方面来看,我在这里写的书都是我希望在我第一次学习 Unity 时就存在的书。Unity 的众多优点之一是它拥有巨大的学习资源宝库,但这些资源往往以不集中的片段形式出现(例如脚本参考或孤立的教程),需要大量挖掘才能找到所需的内容。理想情况下,我会有一本书将我需要知道的所有内容集中在一个地方,并以清晰而合乎逻辑的方式呈现,所以现在我正在为您撰写这样一本书。我的目标是那些已经知道如何编程但刚接触 Unity 的人,也可能是游戏开发方面的新手。项目的选择反映了我通过快速连续地完成各种自由项目来获得技能和信心的经验。
In many ways, what I’ve written here is the book I wish had existed back when I was first learning Unity. Among Unity’s many virtues is a huge treasure trove of learning resources, but those resources tend to take the form of unfocused fragments (like the script reference or isolated tutorials) and require much digging to find what you need. Ideally, I’d have a book that wrapped up everything I needed to know in one place and presented it in a clear and logical manner, so now I’m writing such a book for you. I’m targeting people who already know how to program but who are newcomers to Unity, and possibly new to game development in general. The choice of projects reflects my experience of gaining skills and confidence by doing a variety of freelance projects in rapid succession.
学习使用 Unity 开发游戏,你将踏上一段激动人心的冒险之旅。对我来说,学习如何开发游戏意味着要忍受很多麻烦。而你却有一个优势,那就是可以从一个连贯的资源中学习:这本书!
In learning to develop games using Unity, you’re setting out on an exciting adventure. For me, learning how to develop games meant putting up with a lot of hassle. You, on the other hand, have the advantage of a single coherent resource to learn from: this book!
我要感谢 Manning Publications 给我机会撰写这本书。与我共事的编辑,包括 Robin de Jongh 和 Dan Maharry,在整个过程中给予了我很大的帮助,他们的反馈使这本书更加强大。Becky Whitney 接任了第三版的主要编辑,而 Candace West 担任了第二版的主要编辑。我还要衷心感谢在本书的开发和制作过程中与我合作的许多其他人:项目编辑 Deirdre Hiam、文字编辑 Sharon Wilkey、校对 Jason Everett 和审阅编辑 Mihaela Batinić。
I would like to thank Manning Publications for giving me the opportunity to write this book. The editors I worked with, including Robin de Jongh and especially Dan Maharry, helped me throughout this undertaking, and the book is much stronger for their feedback. Becky Whitney took over as primary editor for this third edition, while Candace West filled that role on the second edition. My sincere thanks also to the many others who worked with me during the development and production of the book: Deirdre Hiam, the project editor; Sharon Wilkey, the copyeditor; Jason Everett, the proofreader; and Mihaela Batinić, the reviewing editor.
我的写作从始至终都受益于审阅者的仔细审查。感谢 Aharon Sharim Rani、Alain Couniot、Alain Lompo、Alberto Simões、Bradley Irby、Brent Boylan、Chris Lundberg、Cristian Antonioli、David Moskowitz、Erik Hansson、Francesco Argese、Hilde Van Gysel、James Matlock、Jan Kroken、John Ackley、John Guthrie、Jose San Leandro、Joseph W. White、Justin Calleja、Kent R. Spillner、Krishna Chaitanya Anipindi、Martin Tidman、Max Weinbrown、Nenko Ivanov Tabakov、Nick Keers、Owain Williams、Robert Walsh、Satej Kumar Sahu、Scott Chaussée 和 Walter Stoneburner。特别感谢技术开发编辑 Scott Chaussee 和技术校对员 Christopher Haupt 的出色审阅工作。 René van den Berg 和 Shiloh Morris 担任了第二版的编辑,而 René 担任了第三版的技术校对,Robin Dewson 负责技术编辑。我还要感谢 Jesse Schell 为我的书撰写前言。
My writing benefited from the scrutiny of reviewers every step of the way. Thanks to Aharon Sharim Rani, Alain Couniot, Alain Lompo, Alberto Simões, Bradley Irby, Brent Boylan, Chris Lundberg, Cristian Antonioli, David Moskowitz, Erik Hansson, Francesco Argese, Hilde Van Gysel, James Matlock, Jan Kroken, John Ackley, John Guthrie, Jose San Leandro, Joseph W. White, Justin Calleja, Kent R. Spillner, Krishna Chaitanya Anipindi, Martin Tidman, Max Weinbrown, Nenko Ivanov Tabakov, Nick Keers, Owain Williams, Robert Walsh, Satej Kumar Sahu, Scott Chaussée, and Walter Stoneburner. Special thanks to the notable review work by technical development editor Scott Chaussee and by technical proofreader Christopher Haupt. René van den Berg and Shiloh Morris stepped into those roles for the second edition, while René was technical proofreader on the third edition and Robin Dewson did the tech edit. And I also want to thank Jesse Schell for writing the foreword to my book.
接下来,我想感谢那些让我的 Unity 体验卓有成效的人。当然,这要从开发 Unity(游戏引擎)的公司 Unity Technologies 开始。我还要感谢 Stack Exchange 上的游戏开发网站 ( https://gamedev.stackexchange.com ) 上的社区;在编写第一版时,我几乎每天都会访问该 QA 网站,向他人学习并回答问题。而促使我使用 Unity 的最大动力来自 Synapse Games 的老板 Alex Reeve。同样,无论是在那份工作中还是之后从事的每一份工作中,我都从同事那里学到了技巧和技术,它们都体现在我编写的代码中。
Next, I’d like to recognize the people who’ve made my experience with Unity a fruitful one. That, of course, starts with Unity Technologies, the company that makes Unity (the game engine). I am also indebted to the community at the Game Development site on Stack Exchange (https://gamedev.stackexchange.com); while writing the first edition, I visited that QA site almost daily to learn from others and to answer questions. And the biggest push for me to use Unity came from Alex Reeve, my boss at Synapse Games. Similarly, I’ve picked up tricks and techniques from my coworkers, in both that and every job I’ve held since, and they all show up in the code I write.
最后,我要感谢我的妻子弗吉尼亚在我写这本书期间的支持。在我开始写这本书之前,我从未真正理解一本书的项目对你的生活和你周围的每个人有多大的影响。非常感谢你的爱和鼓励。
Finally, I want to thank my wife, Virginia, for her support during the time I was writing the book. Until I started working on it, I never really understood how much a book project takes over your life and affects everyone around you. Thank you so much for your love and encouragement.
《Unity in Action,第三版》是一本关于使用 Unity 编程游戏的书。您可以将其视为面向经验丰富的程序员的 Unity 入门书。本书的目标很简单:教那些有一定编程经验但没有 Unity 经验的人如何使用 Unity 开发游戏。
Unity in Action, Third Edition is a book about programming games in Unity. Think of it as an intro to Unity for experienced programmers. The goal of this book is straightforward: to take people who have some programming experience, but no experience with Unity, and teach them how to develop a game by using Unity.
教授开发的最佳方式是通过示例项目,让学生边做边学,这就是本书采用的方法。我将介绍构建示例游戏的步骤,并鼓励您在探索本书的同时使用 Unity 构建这些游戏。我们将每隔几章介绍一些项目,而不是在整本书中开发一个整体项目。(有时其他书籍采用“一个整体项目”的方法,但如果前面的章节与您无关,这可能会让您很难跳到中间部分。)
The best way of teaching development is through example projects, with students learning by doing, and that’s the approach this book takes. I’ll present topics as steps toward building sample games, and you’ll be encouraged to build these games in Unity while exploring the book. We’ll go through a selection of projects every few chapters, rather than one monolithic project developed over the entire book. (Sometimes other books take the “one monolithic project” approach, but that can make it hard to jump into the middle if the early chapters aren’t relevant to you.)
这本书的编程内容比大多数 Unity 书籍(尤其是初学者书籍)更为严谨。Unity 经常被描述为无需编程的功能列表,这是一种误导性的观点,它不会教人们制作商业作品所需的知识。如果你还不知道如何编程,我建议你去各种“免费交互式编码课程”网站(例如https://learnprogramming.online),然后在学习如何编程后再回来阅读这本书。
This book has more rigorous programming content than most Unity books (especially beginners’ books). Unity is often portrayed as a list of features with no programming required, which is a misleading view that won’t teach people what they need to know in order to produce commercial titles. If you don’t already know how to program a computer, I suggest going to one of the various “free interactive coding lessons” websites (https://learnprogramming.online, for example) and then coming back to this book after learning how to program.
不用担心确切的编程语言;本书全书都使用 C#,但其他语言的技能也可以很好地移植。虽然本书的第一部分花了不少时间介绍新概念,并将仔细而谨慎地指导您使用 Unity 开发您的第一款游戏,但其余章节的进度要快得多,以便带您完成多种游戏类型的项目。本书以一章描述部署到各种平台(包括 Web 和移动平台)结束,但本书的主要内容并未提及最终的部署目标,因为 Unity 具有出色的平台无关性。
Don’t worry about the exact programming language; C# is used throughout this book, but skills from other languages will transfer quite well. Although the first part of the book takes its time introducing new concepts and will carefully and deliberately step you through developing your first game in Unity, the remaining chapters move a lot faster in order to take you through projects in multiple game genres. The book ends with a chapter describing deployment to various platforms including the web and mobile, but the main thrust of the book doesn’t make any reference to the ultimate deployment target because Unity is wonderfully platform-agnostic.
至于游戏开发的其他方面,过多地介绍艺术学科会削弱本书的涵盖范围,而且主要涉及 Unity 之外的软件(例如,使用的动画软件)。对艺术任务的讨论将仅限于 Unity 特有的方面或所有游戏开发人员都应该知道的方面。(但请注意,附录 C 是关于建模自定义对象的。)
As for other aspects of game development, extensive coverage of art disciplines would water down how much the book can cover and would be largely about software external to Unity (for example, the animation software used). Discussion of art tasks will be limited to aspects specific to Unity or that all game developers should know. (Note, though, that appendix C is about modeling custom objects.)
第 1 章向您介绍跨平台游戏开发环境 Unity。您将了解 Unity 中所有组件的基础系统,以及如何编写和执行基本脚本。
Chapter 1 introduces you to Unity, the cross-platform game development environment. You’ll learn about the fundamental component system underlying everything in Unity, as well as how to write and execute basic scripts.
第 2 章介绍了编写 3D 运动演示,涵盖鼠标和键盘输入等主题。详细解释了如何定义和操作 3D 位置和旋转。
Chapter 2 progresses to writing a demo of movement in 3D, covering topics like mouse and keyboard input. Defining and manipulating both 3D positions and rotations are thoroughly explained.
第 3 章将移动演示转变为第一人称射击游戏,教你光线投射和基本 AI。光线投射(向场景中投射一条线并查看其相交之处)对于各种游戏来说都是有用的操作。
Chapter 3 turns the movement demo into a first-person shooter, teaching you raycasting and basic AI. Raycasting (shooting a line into the scene and seeing what it intersects) is a useful operation for all sorts of games.
第 4 章介绍如何导入和创建艺术资产。这是本书唯一不关注代码的章节,因为每个项目都需要(基本)模型和纹理。
Chapter 4 covers importing and creating art assets. This is the one chapter of the book that does not focus on code, because every project needs (basic) models and textures.
第 5 章教你如何在 Unity 中创建 2D 益智游戏。尽管 Unity 最初只针对 3D 图形,但现在它对 2D 图形的支持非常出色。
Chapter 5 teaches you how to create a 2D puzzle game in Unity. Although Unity started exclusively for 3D graphics, it now has excellent support for 2D graphics.
第 6 章将通过平台游戏机制扩展 2D 游戏的解释。具体来说,我们将为玩家实现控制、物理和动画。
Chapter 6 expands the 2D game explanations with platform game mechanics. In particular, we’ll implement controls, physics, and animation for the player.
第 7 章向您介绍 Unity 中的最新 GUI 功能。每个游戏都需要一个 UI,最新版本的 Unity 具有改进的 UI 创建系统。
Chapter 7 introduces you to the latest GUI functionality in Unity. Every game needs a UI, and the latest versions of Unity feature an improved system for creating UIs.
第 8 章介绍如何创建另一个 3D 移动演示,这次仅从第三人称视角观看。实现第三人称控制将演示关键的 3D 数学运算,您将学习如何使用动画角色。
Chapter 8 shows how to create another movement demo in 3D, seen only from the third-person perspective this time. Implementing third-person controls will demonstrate key 3D math operations, and you’ll learn how to work with an animated character.
第 9 章将介绍如何在游戏中实现交互式设备和物品。玩家可以通过多种方式操作这些设备,包括直接触摸它们、触摸游戏中的触发器或按下控制器上的按钮。
Chapter 9 goes over how to implement interactive devices and items within your game. The player will have multiple ways of operating these devices, including touching them directly, touching triggers within the game, or pressing a button on the controller.
第 10 章介绍如何与互联网通信。您将学习如何使用标准互联网技术发送和接收数据,例如使用 HTTP 请求从服务器获取 XML 或 JSON 数据。
Chapter 10 covers how to communicate with the internet. You’ll learn how to send and receive data by using standard internet technologies, like HTTP requests to get XML or JSON data from a server.
第 11 章讲授如何编写音频功能。Unity 对短音效和长音乐曲目都提供了很好的支持;这两种音频对于几乎所有视频游戏都至关重要。
Chapter 11 teaches how to program audio functionality. Unity has great support for both short sound effects and long music tracks; both sorts of audio are crucial for almost all video games.
第 12 章将指导您如何将不同章节的内容整合到一个游戏中。此外,您还将学习如何编写点击控件以及如何保存玩家的游戏。
Chapter 12 walks you through bringing together pieces from different chapters into a single game. In addition, you’ll learn how to program point-and-click controls and how to save the player’s game.
第 13 章介绍了如何构建最终应用,以及如何部署到桌面、Web、移动设备甚至 VR 等多个平台。Unity 让您能够为每个主要游戏平台创建游戏!
Chapter 13 goes over building the final app, with deployment to multiple platforms like desktop, web, mobile, and even VR. Unity enables you to create games for every major gaming platform!
四个附录提供了有关场景导航、外部工具、Blender 和学习资源的附加信息。
Four appendixes provide additional information about scene navigation, external tools, Blender, and learning resources.
本书中的所有源代码(无论是代码清单还是代码片段)都采用等宽 字体 (如 ),使其与周围的文本区分开来。在大多数清单中,代码都带有注释以指出关键概念。代码的格式经过调整,通过添加换行符和谨慎使用缩进,使其适合书中可用的页面空间。
All the source code in the book, whether in code listings or snippets, is in a fixed-width font like this, which sets it off from the surrounding text. In most listings, the code is annotated to point out key concepts. The code is formatted so that it fits within the available page space in the book by adding line breaks and using indentation carefully.
唯一需要的软件是 Unity;本书使用 Unity 2020.3.12,这是我撰写本文时的当前默认版本。某些章节偶尔会讨论其他软件,但这些被视为可选附加内容,而不是您所学习内容的核心。
The only software required is Unity; this book uses Unity 2020.3.12, which is the current default release as I write this. Certain chapters do occasionally discuss other pieces of software, but those are treated as optional extras and not core to what you’re learning.
警告Unity 项目会记住它们是在哪个 Unity 版本中创建的,如果您尝试在其他版本中打开它们,则会发出警告。如果您在打开本书的示例下载时看到该警告,请单击“继续”并忽略它。
WARNING Unity projects remember which version of Unity they were created in and will issue a warning if you attempt to open them in a different version. If you see that warning while opening this book’s sample downloads, click Continue and ignore it.
本书中散布的代码清单通常显示了在现有代码文件中添加或更改的内容;除非这是给定代码文件的首次出现,否则不要用后续清单替换整个文件。虽然您可以下载完整的工作示例项目以供参考,但最好输入代码清单并仅查看工作示例以供参考。这些下载可从出版商的网站(https://www.manning.com/books/unity-in-action-third-edition)和 GitHub(https://github.com/jhocking/uia-3e)获得。
The code listings sprinkled throughout the book generally show what to add or change in existing code files; unless it’s the first appearance of a given code file, don’t replace the entire file with subsequent listings. Although you can download complete working sample projects to refer to, you’ll learn best by typing out the code listings and looking at the working samples only for reference. Those downloads are available from the publisher’s website (https://www.manning.com/books/unity-in-action-third-edition) and on GitHub (https://github.com/jhocking/uia-3e).
购买《Unity in Action》第三版,即可免费访问 Manning 的在线阅读平台 liveBook。使用 liveBook 独有的讨论功能,您可以对本书全局或特定章节或段落发表评论。您可以轻而易举地为自己做笔记、提出和回答技术问题以及获得作者和其他用户的帮助。要访问论坛,请访问https://livebook.manning.com/#!/book/unity-in-action-third-edition/discussion 。您还可以在https://livebook.manning.com/#!/discussion上了解有关 Manning 论坛和行为准则的更多信息。
Purchase of Unity in Action, Third Edition, includes free access to liveBook, Manning’s online reading platform. Using liveBook’s exclusive discussion features, you can attach comments to the book globally or to specific sections or paragraphs. It’s a snap to make notes for yourself, ask and answer technical questions, and receive help from the author and other users. To access the forum, go to https://livebook.manning.com/#!/book/unity-in-action-third-edition/discussion. You can also learn more about Manning's forums and the rules of conduct at https://livebook.manning.com/#!/discussion.
Mannings 对我们读者的承诺是提供一个平台,让读者之间以及读者与作者之间能够进行有意义的对话。这并不是对作者参与论坛的任何具体次数的承诺,作者对论坛的贡献仍然是自愿的(并且是无偿的)。我们建议您尝试向作者提出一些有挑战性的问题,以免他失去兴趣!只要这本书还在印刷中,就可以从出版商的网站上访问论坛和以前的讨论档案。
Mannings’s commitment to our readers is to provide a venue where a meaningful dialogue between individual readers and between readers and the author can take place. It is not a commitment to any specific amount of participation on the part of the author, whose contribution to the forum remains voluntary (and unpaid). We suggest you try asking the author some challenging questions lest his interest stray! The forum and the archives of previous discussions will be accessible from the publisher’s website as long as the book is in print.
Joe Hocking是一位软件工程师,专门从事交互式媒体开发。他目前就职于高通公司,在 BUNDLAR 工作期间编写了第三版的大部分内容,在 Synapse Games 工作期间编写了第一版。他还曾在伊利诺伊大学芝加哥分校、芝加哥艺术学院和芝加哥哥伦比亚学院授课。他与妻子和两个孩子住在芝加哥郊区。他的网站是www.newarteest.com。
Joe Hocking is a software engineer who specializes in interactive media development. He currently works for Qualcomm, wrote most of the third edition while working for BUNDLAR, and wrote the first edition while at Synapse Games. He has also taught classes at the University of Illinois Chicago, the School of the Art Institute of Chicago, and Columbia College Chicago. He lives in the Chicago suburbs with his wife and two kids. His website is www.newarteest.com.
《团结在行动》第三版封面上的插图题为“大君司仪的服饰”。大君是奥斯曼帝国苏丹的另一个名字。插图取自托马斯·杰弗里斯 (Thomas Jefferys) 所著的《古代和现代不同国家服饰集》,该书于 1757 年至 1772 年间在伦敦出版。标题页指出,这些是手工上色的铜版画,并用阿拉伯胶加深。杰弗里斯 (1719-1771) 被称为“乔治三世国王的地理学家”。杰弗里斯是一位英国制图师,是当时领先的地图供应商,他为政府和其他官方机构雕刻和印刷地图,并制作了各种商业地图和地图集,尤其是北美地图。他作为地图绘制者的工作激发了人们对他所调查地区的当地服饰习俗的兴趣,这些习俗在这本四卷合集中得到了精彩展示。
The figure on the cover of Unity in Action, Third Edition is captioned “Habit of the Master of Ceremonies of the Grand Signior.” The Grand Signior was another name for a sultan of the Ottoman Empire. The illustration is taken from A Collection of the Dresses of Different Nations, Ancient and Modern by Thomas Jefferys, published in London between 1757 and 1772. The title page states that these are hand-colored copperplate engravings, heightened with gum arabic. Jefferys (1719-1771) was called “Geographer to King George III.” An English cartographer who was the leading map supplier of his day, Jefferys engraved and printed maps for government and other official bodies and produced a wide range of commercial maps and atlases, especially of North America. His work as a mapmaker sparked an interest in local dress customs of the lands he surveyed, which are brilliantly displayed in this four-volume collection.
18 世纪后期,对遥远国度的迷恋和为乐趣而旅行是相对较新的现象,而像这样的画集很受欢迎,让游客和坐在扶手椅上的旅行者了解其他国家的居民。杰弗里斯画集中的绘画种类繁多,生动地展现了 200 年前世界各国的独特性和个性。从那时起,着装规范发生了变化,当时如此丰富的地区和国家多样性已逐渐消失。现在很难区分一个大陆的居民和另一个大陆的居民。也许,如果我们试着乐观地看待它,我们已经用文化和视觉多样性换取了更加多样化的个人生活,或者更加多样化和有趣的知识和技术生活。
Fascination with faraway lands and travel for pleasure were relatively new phenomena in the late 18th century, and collections such as this one were popular, introducing the tourist as well as the armchair traveler to the inhabitants of other countries. The diversity of the drawings in Jefferys’s volumes speaks vividly of the uniqueness and individuality of the world’s nations some 200 years ago. Dress codes have changed since then, and the diversity by region and country, so rich at the time, has faded away. It is now hard to tell the inhabitant of one continent apart from another. Perhaps, trying to view it optimistically, we have traded a cultural and visual diversity for a more varied personal life, or a more varied and interesting intellectual and technical life.
在如今这个计算机书籍已经很难区分的时代,曼宁通过基于两个世纪前丰富多样的地域生活的封面来赞美计算机行业的创造性和主动性,并通过杰弗里斯的图片将其重新呈现。
At a time when it is hard to tell one computer book from another, Manning celebrates the inventiveness and initiative of the computer business with book covers based on the rich diversity of regional life of two centuries ago, brought back to life by Jefferys’s pictures.
现在是时候开始使用 Unity 了。如果你对 Unity 一无所知,没关系!我将首先解释什么是 Unity,包括如何在其中编写游戏的基础知识。然后我们将介绍一个关于在 Unity 中开发简单游戏的教程。这个第一个项目将教你几种特定的游戏开发技术,并让你很好地了解该过程的工作原理。继续第 1 章!
It’s time to take your first steps in using Unity. If you don’t know anything about Unity, that’s okay! I’m going to start by explaining what Unity is, including the fundamentals of how to program games in it. Then we’ll walk through a tutorial about developing a simple game in Unity. This first project will teach you several specific game development techniques, as well as give you a good overview of how the process works. Onward to chapter 1!
如果你和我一样,那么你早就想开发一款电子游戏了。但从玩游戏到制作游戏,这是一个巨大的飞跃。多年来,出现了许多游戏开发工具,我们将讨论其中最新、最强大的工具之一。
If you’re anything like me, you’ve had developing a video game on your mind for a long time. But it’s a big jump from playing games to making them. Numerous game development tools have appeared over the years, and we’re going to discuss one of the most recent and most powerful of these tools.
Unity是一款专业级的游戏引擎,用于创建针对各种平台的视频游戏。它不仅是成千上万经验丰富的游戏开发者每天使用的专业开发工具,也是新手游戏开发者最容易使用的现代工具之一。直到最近,游戏开发新手一开始都会面临许多巨大的障碍,但 Unity 让学习这些技能变得容易。
Unity is a professional-quality game engine used to create video games targeting a variety of platforms. It’s not only a professional development tool used daily by thousands of seasoned game developers, but also one of the most accessible modern tools for novice game developers. Until recently, a newcomer to game development would face lots of imposing barriers right from the start, but Unity makes it easy to start learning these skills.
因为您正在阅读这本书,所以您很可能对计算机技术感到好奇,并且曾经使用其他工具开发过游戏,或者构建过其他类型的软件,例如桌面应用程序或网站。创建视频游戏与编写任何其他类型的软件并没有根本区别;两者的区别主要在于程度。例如,视频游戏比大多数网站更具互动性,因此涉及不同类型的代码,但创建两者所需的技能和流程是相似的。
Because you’re reading this book, chances are you’re curious about computer technology and have either developed games with other tools or built other kinds of software, such as desktop applications or websites. Creating a video game isn’t fundamentally different from writing any other kind of software; it’s mostly a difference of degree. For example, a video game is a lot more interactive than most websites, and thus involves different sorts of code, but the skills and processes involved in creating both are similar.
如果您已经扫清了学习游戏开发道路上的第一个障碍,学习了编程软件的基础知识,那么下一步就是选择一些游戏开发工具并将编程知识运用到游戏领域。Unity 是游戏开发环境的绝佳选择。
If you’ve already cleared the first hurdle on your path to learning game development, having learned the fundamentals of programming software, then your next step is to pick up some game development tools and translate that programming knowledge into the realm of gaming. Unity is a great choice of game development environment to work with.
首先,请访问www.unity.com了解有关该软件的更多信息。尽管 Unity 最初的重点是 3D 游戏,但 Unity 也非常适合 2D 游戏,本书涵盖了这两种游戏。事实上,即使在 3D 项目上演示,许多主题(保存数据、播放音频等)也适用于这两种游戏。第 1.2 节将指导您作为新手安装 Unity,但首先让我们讨论一下选择此工具的具体原因。
To start, go to www.unity.com to learn more about the software. Although Unity’s original focus was on 3D games, Unity works great for 2D games as well, and this book covers both. Indeed, even when demonstrated on a 3D project, many topics (saving data, playing audio, and so on) apply to both. Section 1.2 will walk you through installing Unity as a newcomer, but first let’s discuss specific reasons to choose this tool.
让我们仔细看看本章开头的描述:Unity 是一款专业品质的游戏引擎,用于创建针对各种平台的视频游戏。这是对“什么是 Unity?”这个简单问题的相当直接的回答。但这个答案到底是什么意思,为什么 Unity 如此出色?
Let’s take a closer look at that description from the beginning of the chapter: Unity is a professional-quality game engine used to create video games targeting a variety of platforms. That’s a fairly straightforward answer to the straightforward question “What is Unity?” But what exactly does that answer mean, and why is Unity so great?
游戏引擎提供了大量适用于多种游戏的功能。使用特定引擎实现的游戏将获得所有这些功能,同时添加特定于该游戏的自定义艺术资产和游戏代码。Unity 具有物理模拟、法线贴图、屏幕空间环境光遮蔽(SSAO)、动态阴影……还有很多。许多游戏引擎都拥有这样的功能,但 Unity 与类似的尖端游戏开发工具相比有两个主要优势:极其高效的视觉工作流程和高度的跨平台支持。
Game engines provide a plethora of features that are useful across many games. A game implemented using a particular engine will get all those features, while adding custom art assets and gameplay code specific to that game. Unity has physics simulation, normal maps, screen space ambient occlusion (SSAO), dynamic shadows . . . and the list goes on. Many game engines boast such features, but Unity has two main advantages over similar cutting-edge game development tools: an extremely productive visual workflow and a high degree of cross-platform support.
可视化工作流程是一种相当独特的设计,不同于大多数其他游戏开发环境。而其他游戏开发工具通常是由必须处理的不同部分组成的复杂大杂烩,或者是一个需要您设置自己的集成开发环境的编程库(IDE)、构建链等等,Unity 中的开发工作流程由复杂的可视化编辑器支撑。
The visual workflow is a fairly unique design, different from most other game development environments. Whereas other game development tools are often a complicated mishmash of disparate parts that must be wrangled, or perhaps a programming library that requires you to set up your own integrated development environment (IDE), build-chain, and whatnot, the development workflow in Unity is anchored by a sophisticated visual editor.
该编辑器用于布置游戏中的场景,并将艺术资产和代码整合到交互式对象中。该编辑器的优点在于,它能够快速高效地构建专业品质的游戏,为开发人员提供工具,让他们能够高效工作,同时还能使用大量最新视频游戏技术。
The editor is used to lay out the scenes in your game and to tie together art assets and code into interactive objects. The beauty of this editor is that it enables professional-quality games to be built quickly and efficiently, giving developers tools to be incredibly productive, while still using an extensive list of the latest technologies in video gaming.
注意:大多数其他具有中央可视化编辑器的游戏开发工具也受到脚本支持有限且不灵活的困扰,但 Unity 不会遭受这种缺点。虽然为 Unity 创建的所有内容最终都会经过可视化编辑器,但此核心界面可用于将项目链接到在 Unity 游戏引擎中运行的自定义代码。经验丰富的程序员不应忽视这个开发环境,误以为它是编程能力有限的一些点击式游戏创建者!
NOTE Most other game development tools that have a central visual editor are also saddled with limited and inflexible scripting support, but Unity doesn’t suffer from that disadvantage. Although everything created for Unity ultimately goes through the visual editor, this core interface can be used to link projects to custom code that runs in Unity’s game engine. Experienced programmers shouldn’t dismiss this development environment, mistaking it for some click-together game creator with limited programming capability!
编辑器对于快速迭代、通过原型设计和测试周期完善游戏特别有用。您可以在编辑器中调整对象,甚至可以在游戏运行时移动对象。此外,Unity 允许您通过编写脚本来自定义编辑器本身,从而向界面添加新功能和菜单。
The editor is especially helpful for doing rapid iteration, honing the game through cycles of prototyping and testing. You can adjust objects in the editor and move things around even while the game is running. Plus, Unity allows you to customize the editor itself by writing scripts that add new features and menus to the interface.
除了编辑器显著的生产力优势之外,Unity 工具集的另一个主要优势是高度的跨平台支持。Unity 不仅在部署目标方面是多平台的(您可以部署到 PC、Web、移动设备或控制台),而且在开发工具方面也是多平台的(您可以在 Microsoft Windows 或 Apple macOS 上开发游戏)。这种平台无关性主要是因为 Unity 最初是 Mac 专用软件,后来移植到 Windows。第一个版本于 2005 年推出,最初仅支持 Mac,但几个月后 Unity 就已更新为在 Windows 上运行。
Besides the editor’s significant productivity advantages, the other main strength of Unity’s tool set is a high degree of cross-platform support. Not only is Unity multiplatform in terms of deployment targets (you can deploy to PC, web, mobile, or consoles), but it’s also multiplatform in terms of development tools (you can develop a game on Microsoft Windows or Apple macOS). This platform-agnostic nature is largely because Unity started as Mac-only software and was later ported to Windows. The first version launched in 2005 and initially supported only Mac, but within months Unity had been updated to work on Windows as well.
后续版本逐渐添加了更多部署平台,例如 2006 年的跨平台 Web 播放器、2008 年的 iPhone、2010 年的 Android,甚至 Xbox 和 PlayStation 等游戏机。最近,Unity 添加了对 WebGL(Web 浏览器中图形的新框架)的部署,甚至还支持扩展现实(XR) — 虚拟现实 (VR) 和增强现实 (AR) — Oculus 和 VIVE 等平台。很少有游戏引擎像 Unity 一样支持如此多的部署目标,也没有哪个引擎能够如此简单地将游戏部署到多个平台。
Successive versions gradually added more deployment platforms, such as a cross-platform web player in 2006, iPhone in 2008, Android in 2010, and even game consoles like Xbox and PlayStation. More recently, Unity has added deployment to WebGL, the new framework for graphics in web browsers, and even has support for extended reality (XR)—both virtual reality (VR) and augmented reality (AR)—platforms like Oculus and VIVE. Few game engines support as many deployment targets as Unity, and none make deploying to multiple platforms so simple.
除了这些主要优势之外,第三个更微妙的优势来自用于构建游戏对象的模块化组件系统。在组件系统中,组件是混合搭配的功能包,对象是作为组件集合构建的,而不是严格的类层次结构。组件系统是一种不同的(通常更灵活的)面向对象编程 (OOP) 方法,它通过组合而不是继承来构建游戏对象。图 1.1 显示了示例比较。
In addition to these main strengths, a third, more subtle, benefit comes from the modular component system used to construct game objects. In a component system, components are mix-and-match packets of functionality, and objects are built up as a collection of components, rather than as a strict hierarchy of classes. A component system is a different (and usually more flexible) approach to object-oriented programming (OOP) that constructs game objects through composition rather than inheritance. Figure 1.1 diagrams an example comparison.
Figure 1.1 Inheritance versus composition
在组件系统中,对象存在于扁平层次结构中,不同的对象具有不同的组件集合。相反,继承结构在树的完全不同的分支上具有不同的对象。组件排列有助于快速进行原型设计,因为您可以快速混合和匹配组件,而不必在对象更改时重构继承链。
In a component system, objects exist on a flat hierarchy, and different objects have different collections of components. An inheritance structure, in contrast, has different objects on completely different branches of the tree. The component arrangement facilitates rapid prototyping, because you can quickly mix and match components rather than having to refactor the inheritance chain when objects change.
虽然您可以编写代码来实现自定义组件系统(如果不存在),但 Unity 已经拥有强大的组件系统,并且该系统甚至与可视化编辑器集成。您不仅可以在代码中操作组件,还可以在可视化编辑器中附加和分离组件。同时,您不仅限于通过组合来构建对象;您仍然可以选择在代码中使用继承,包括基于继承出现的所有最佳实践设计模式遗产。
Although you could write code to implement a custom component system if one didn’t exist, Unity already has a robust component system, and this system is even integrated with the visual editor. Instead of being able to manipulate components only in code, you can attach and detach components within the visual editor. Meanwhile, you aren’t limited to building objects only through composition; you still have the option of using inheritance in your code, including all the best-practice design patterns that have emerged based on inheritance.
统一具有许多优点,使其成为开发游戏的绝佳选择,我强烈推荐它,但如果我不提它的缺点,那我就太失职了。特别是,可视化编辑器和复杂编码的结合,虽然对 Unity 的组件系统非常有效,但却是不寻常的,可能会造成困难。在复杂的场景中,您可能会忘记场景中哪些对象附加了特定的组件。Unity 确实提供了用于查找附加脚本的搜索功能,但它可以更强大;有时您仍然会遇到需要您手动检查场景中的所有内容以找到脚本链接的情况。这种情况并不经常发生,但一旦发生,就会很乏味。
Unity has many advantages that make it a great choice for developing games, and I highly recommend it, but I’d be remiss if I didn’t mention its weaknesses. In particular, the combination of the visual editor and sophisticated coding, though very effective with Unity’s component system, is unusual and can create difficulties. In complex scenes, you can lose track of which objects in the scene have specific components attached. Unity does provide a search feature for finding attached scripts, but it could be more robust; sometimes you still encounter situations requiring you to manually inspect everything in the scene in order to find script linkages. This doesn’t happen often, but when it does happen, it can be tedious.
另一个令有经验的程序员感到惊讶和沮丧的缺点是链接外部代码库可能很困难。旧版本的 Unity 实际上根本不支持外部代码库,因此必须手动将它们复制到每个项目中。现在 Unity 附带了包管理器,并且可以从中央共享位置引用库(或包)。这些包对于 Unity 本身提供的可选功能非常有用(Unity 不会自动包含您在每个项目中不需要的功能),并且后续章节偶尔会让您安装诸如高级字体处理之类的包。但是,创建自己的包可能很棘手,这使得在多个项目之间共享代码变得很尴尬。您可能会发现只需在项目之间手动复制代码并在以后处理任何版本不匹配的问题更简单,但这不是一个理想的权衡。
Another disadvantage that can be surprising and frustrating for experienced programmers is that linking in external code libraries can be difficult. Old versions of Unity didn’t support external code libraries at all actually, so they had to be manually copied into every project. Now Unity comes with the Package Manager, and libraries (or packages) are referenced from a central shared location. These packages work great for optional functionality provided by Unity itself (Unity doesn’t automatically include functionality that you don’t need in every single project), and future chapters will occasionally have you installing packages for things like advanced font handling. Creating your own packages can be tricky, however, making it awkward to share code among multiple projects. You may find it simpler to just manually copy code between projects and deal with any version mismatches down the road, which is not an ideal trade-off to be making.
注意:难以使用版本控制系统(如 Git 或 Subversion)曾经是 Unity 的一个重大弱点,但较新的版本可以正常工作。您可能会发现过时的资源告诉您 Unity 不支持版本控制,但较新的资源描述了项目中哪些文件和文件夹需要放入存储库,哪些不需要。首先,请阅读 Unity 的文档(http://mng.bz/BbhD)或查看 GitHub 维护的 .gitignore 文件(http://mng.bz/g7nl)。
NOTE Difficulty working with version-control systems (such as Git or Subversion) used to be a significant weakness of Unity, but more recent versions work fine. You may find out-of-date resources telling you that Unity doesn’t work with version control, but newer resources describe which files and folders in a project need to be put in the repository and which don’t. To start out, read Unity’s documentation (http://mng.bz/BbhD) or look at the .gitignore file maintained by GitHub (http://mng.bz/g7nl).
第三个缺点与有时令人眼花缭乱的选项有关。Unity 为某些功能提供了多种方法,但并不总是清楚应该使用哪种方法。在一定程度上,这种情况对于正在积极开发的工具来说是不可避免的,但仍会给用户带来困惑和不适。这种进化的混乱甚至会让 Unity 老手感到困惑,因此 Unity 新手有时肯定会感到困惑。本书重点介绍了这些功能并提供指导。
A third weakness has to do with the sometimes dizzying array of options. Unity offers multiple approaches to some functionalities, and it is not always clear which approach you should use. To a certain extent, that situation is inevitable for a tool under active development, but still results in confusion and discomfort for users. This evolutionary messiness can bewilder even Unity veterans, so newcomers to Unity will definitely face confusion at times. This book highlights such features and offers guidance.
例如,第 7 章解释如何开发用户界面(UI)用于 Unity 游戏。实际上,Unity 有三个UI 系统(在http://mng.bz/r60X上进行了比较),因为相继开发的系统改进了其前身。本书介绍了第二个 UI 系统(Unity UI 或 uGUI),因为它仍然比不完整的第三个 UI 系统(UI Toolkit)更受欢迎,但如果 UI Toolkit 在几年内成熟到可以投入生产,我也不会感到惊讶。在此期间,新手可能难以决定使用哪个 UI方法。
For example, chapter 7 explains how to develop a user interface (UI) for Unity games. Well, Unity actually has three UI systems (which are compared at http:// mng.bz/r60X) because of successively developed systems that improve on their predecessor. This book covers the second UI system (Unity UI, or uGUI) because it is still preferred over the incomplete third UI system (UI Toolkit), but I wouldn’t be surprised if UI Toolkit matures to production-ready within a few years. In the interim, newcomers may have difficulty deciding on a UI approach.
你已经听说过 Unity 的优缺点,但您可能仍需要说服其开发工具可以提供一流的结果。访问 Unity 画廊 https://unity.com/case-study,查看使用 Unity 开发的游戏和模拟的不断更新列表。本节探讨了一些游戏,展示了多种类型和部署平台。所有游戏名称均为其各自游戏公司的商标,屏幕截图也归这些公司所有,保留所有权利。
You’ve heard about the pros and cons of Unity, but you might still need convincing that its development tools can give first-rate results. Visit the Unity gallery at https://unity.com/case-study to see a constantly updated list of games and simulations developed using Unity. This section explores a handful of games, showcasing multiple genres and deployment platforms. All game titles are trademarks of their respective game companies, and screenshots are also copyrighted to those companies, with all rights reserved.
桌面(Windows、Mac、Linux)和控制台(PlayStation、Xbox、Switch)
Desktop (Windows, Mac, Linux) and Console (PlayStation, Xbox, Switch)
因为这Unity 编辑器在同一平台上运行,部署到 Windows 或 Mac 通常是最直接的目标平台。同时,由于 Unity 易于跨平台部署,使用 Unity 开发的主机游戏通常也会在 PC 上发布。以下是不同类型的桌面和主机游戏的几个示例:
Because the Unity editor runs on the same platform, deployment to Windows or Mac is often the most straightforward target platform. Meanwhile, console games developed in Unity are often released on PC too, thanks to Unity’s easy cross-platform deployment. Here are a couple of examples of desktop and console games in different genres:
Fall Guys(图 1.2),一款由 Mediatonic(Mediatonic Limited 的商标)开发的混乱 3D 动作游戏
Fall Guys (figure 1.2), a chaotic 3D action game developed by Mediatonic (trademarks of Mediatonic Limited)
统一还可以将游戏部署到 iOS(iPhone 和 iPad)和 Android(手机和平板电脑)等移动平台。以下是不同类型的移动游戏的三个示例:
Unity can also deploy games to mobile platforms like iOS (iPhones and iPads) and Android (phones and tablets). Here are three examples of mobile games in different genres:
Figure 1.6 Animation Throwdown
虚拟现实(Oculus、VIVE、PlayStation VR)
Virtual Reality (Oculus, VIVE, PlayStation VR)
统一甚至可以部署到 XR 平台,包括虚拟现实耳机。以下是不同类型的 VR 游戏的几个示例:
Unity can even deploy to XR platforms, including virtual reality headsets. Here are a couple of examples of VR games in different genres:
Figure 1.8 I Expect You to Die
从这些例子中可以看出,Unity 的优势绝对可以转化为商业品质的游戏。但即使 Unity 比其他游戏开发工具具有显著优势,新手也可能误解编程在开发过程中的作用。
As you can see from these examples, Unity’s strengths can definitely translate into commercial-quality games. But even with Unity’s significant advantages over other game development tools, newcomers may misunderstand the involvement of programming in the development process.
Unity 经常被描述为无需编程即可实现一系列功能,这是一种误导性的观点,它不会教给人们制作商业游戏所需的知识。虽然你可以使用现有的组件制作出一个相当复杂的原型,甚至不需要程序员的参与(这本身就是一项了不起的成就),但要想将有趣的原型变成精致的游戏,就需要严格的编程准备好以供释放。
Unity is often portrayed as a list of features with no programming required, which is a misleading view that won’t teach people what they need to know in order to produce commercial titles. Though it’s true that you can click together a fairly elaborate prototype using preexisting components even without a programmer being involved (which is itself a pretty big feat), rigorous programming is required to move beyond an interesting prototype to a polished game ready for release.
这上一节讨论了很多关于 Unity 可视化编辑器的生产力优势,现在让我们来看看它的界面是什么样子的,以及它是如何工作的。如果你还没有下载程序,请访问www.unity.com并单击“开始”按钮。在这里,你会看到提供的各种订阅计划的细目分类。本书中的所有内容都适用于免费版,因此请选择“个人”选项卡并单击免费个人版下的按钮。Unity 的付费版本主要在商业许可条款上有所不同,而不是在底层功能上有所不同。
The previous section talked a lot about the productivity benefits of Unity’s visual editor, so let’s go over what the interface looks like and how it operates. If you haven’t already done so, download the program by going to www.unity.com and clicking Get Started. Here you will see a breakdown of the various subscription plans offered. Everything in this book works in the free version, so select the Individual tab and click the button under the free Personal edition. The paid versions of Unity differ mainly in commercial licensing terms, not in underlying functionality.
该网站为新用户和回访用户提供单独的下载。区别在于,新用户的下载将启动一个软件向导,引导用户进入入门教程,而回访用户的下载则直接进入主应用程序,没有任何介绍。因此,即使您是 Unity 新手,也可以获取回访用户的下载并跳过简介内容(毕竟,这本书中简介内容是多余的)。
The website has separate downloads for new and returning users. The difference is simply that the download for new users will launch into a software wizard that directs users to intro tutorials, whereas the download for returning users goes straight to the main application with no introduction. So even if you are new to Unity, get the download for returning users and skip the intro content (it’s redundant with this book, after all).
实际上,您将下载一个轻量级安装管理器,而不是主 Unity 应用程序。这个管理器应用程序称为Unity Hub,它的存在是为了简化同时安装和使用多个版本的 Unity。如图 1.9 所示,启动 Unity Hub 时,首先要安装编辑器。安装默认的推荐版本;本书使用Unity 2020.3.12(截至撰写本文时,这是当前的默认版本)。如果您稍后想要安装其他版本的 Unity(有比默认版本更新的版本可用),请单击 Unity Hub 侧面菜单上的 Installs。
You’ll actually download a lightweight installation manager rather than the main Unity application. This manager application, called Unity Hub, exists to simplify the installation and use of multiple versions of Unity simultaneously. As shown in figure 1.9, installing the editor will be the first thing that happens when you launch Unity Hub. Install whichever is the default Recommended Release; this book uses Unity 2020.3.12 (the current default release as of this writing). If you later want to install additional versions of Unity (newer versions than the default are available), click Installs on the side menu in Unity Hub.
Figure 1.9 Unity Hub on first launch versus subsequently
提示当你读到这篇文章时,Unity 的新版本可能已经发布了。高级功能将会改变,甚至界面的外观也可能不同,但本书涵盖的基本概念仍然适用。本书中给出的解释通常仍然适用于 Unity 的未来版本。
TIP By the time you read this, newer Unity versions will likely have been released. Advanced features will have changed, and possibly even the look of the interface could be different, but the fundamental concepts covered by this book will still be true. The explanations given in this book will generally still apply to whichever future version of Unity is current.
警告项目会记住它们是在哪个 Unity 版本中创建的,如果您尝试在其他版本中打开它们,则会发出警告。有时这无关紧要(例如,如果在打开本书的示例下载时出现警告,请忽略它),但有时您不想在错误的版本中打开项目。
WARNING Projects remember which version of Unity they were created in and will issue a warning if you attempt to open them in a different version. Sometimes it doesn’t matter (for example, ignore the warning if it appears while opening this book’s sample downloads), but sometimes you don’t want to open a project in the wrong version.
继续安装编辑器,转到“学习”选项卡下载第一个项目。选择任何项目进行查看(无论如何你不会用它做很多事情),但请注意,图 1.10 显示的是 Karting。Unity 将下载并启动所选项目。您可能会看到一条关于导入文件以设置新项目的警告消息;请注意导入可能需要几分钟。
Continuing on from installing the editor, go to the Learn tab to download a first project. Select any project to look around in (you won’t be doing much with it anyway) but note that figure 1.10 shows Karting. Unity will download and launch the selected project. You may see a warning message about importing files to set up the new project; realize that the import can take several minutes.
新项目最终加载完成后,选择“加载场景”以关闭初始弹出窗口。如果尚未打开,请在编辑器底部的文件浏览器中导航到 Assets/Karting/Scenes/,然后双击 MainScene(场景文件有 Unity 立方体图标)。您应该会看到类似于图 1.10 的屏幕。
Once the new project is finally loaded, choose Load Scene to dismiss the initial pop-up. If it isn’t already open, navigate to Assets/Karting/Scenes/ in the file browser at the bottom of the editor, and double-click MainScene (scene files have the Unity cube icon). You should see a screen similar to figure 1.10.
Figure 1.10 Parts of the interface in Unity
Unity 中的界面分为几个部分:场景选项卡、游戏选项卡、工具栏、层次结构选项卡、检查器、项目选项卡和控制台选项卡。每个部分都有不同的用途,但对于游戏构建生命周期都至关重要:
The interface in Unity is split into sections: the Scene tab, the Game tab, the Toolbar, the Hierarchy tab, the Inspector, the Project tab, and the Console tab. Each section has a different purpose, but all are crucial to the game-building life cycle:
You can position objects in the current scene by using the Scene tab.
You can drag and drop object relationships in the Hierarchy tab.
The Inspector lists information about selected objects, including linked code.
You can test playing in Game view while watching error output in the Console tab.
这是 Unity 中的默认布局;所有视图都位于选项卡中,可以移动或调整大小,停靠在屏幕上的不同位置。稍后,您可以尝试自定义布局,但目前,默认布局是了解所有视图功能的最佳方式。
This is the default layout in Unity; all of the views are in tabs and can be moved around or resized, docking in different places on the screen. Later, you can play around with customizing the layout, but for now, the default layout is the best way to understand what all the views do.
这界面最突出的部分是中间的场景视图。在这里,您可以看到游戏世界是什么样子,并移动物体。场景中的网格对象显示为它们的网格(稍后定义)。您还可以看到场景中的其他对象,它们由图标和彩色线条表示:摄像机、灯光、音频源、碰撞区域等。请注意,您在此处看到的视图与正在运行的游戏中看到的视图不同 - 您可以随意环顾场景,而不受游戏视图的限制。
The most prominent part of the interface is the Scene view in the middle. This is where you can see what the game world looks like and move objects around. Mesh objects in the scene appear as, well, their mesh (defined in a moment). You can also see other objects in the scene, represented by icons and colored lines: cameras, lights, audio sources, collision regions, and so forth. Note that the view you’re seeing here isn’t the same as the view in the running game—you’re able to look around the scene at will without being constrained to the game’s view.
定义网格对象是空间中的视觉对象。3D 图形中的视觉效果由大量相连的线条和形状构成,因此有“网格”一词。
DEFINITION A mesh object is a visual object in space. Visuals in 3D graphics are constructed out of lots of connected lines and shapes—hence the word mesh.
游戏视图不是屏幕的独立部分,而是位于场景旁边的另一个选项卡(查找视图左上角的选项卡)。界面中的几个地方有多个这样的选项卡;如果您单击其他选项卡,视图将被新的活动选项卡替换。游戏运行时,您在此视图中看到的就是游戏。每次运行游戏时都无需手动切换选项卡,因为游戏开始时视图会自动切换到游戏。
The Game view isn’t a separate part of the screen but rather another tab located right next to Scene (look for tabs at the top left of views). A couple of places in the interface have multiple tabs like this; if you click a different tab, the view is replaced by the new active tab. When the game is running, what you see in this view is the game. It isn’t necessary to manually switch tabs every time you run the game, because the view automatically switches to Game when the game starts.
提示:游戏运行时,您可以切换回“场景”视图,以便检查正在运行的场景中的对象。此功能对于查看游戏运行时发生的情况非常有用,并且是大多数游戏引擎中不提供的有用调试工具。
TIP While the game is running, you can switch back to the Scene view, allowing you to inspect objects in the running scene. This capability is extremely useful for seeing what’s going on while the game is running and is a helpful debugging tool that isn’t available in most game engines.
说到运行游戏,这很简单,只需单击“场景”视图上方的“播放”按钮即可。界面的整个顶部部分称为工具栏,而“播放”位于中间。图 1.11 将整个编辑器界面拆分开来,仅显示顶部的工具栏以及正下方的“场景/游戏”选项卡。
Speaking of running the game, that’s as simple as clicking the Play button just above the Scene view. That whole top section of the interface is referred to as the Toolbar, and Play is located right in the middle. Figure 1.11 breaks apart the full editor interface to show only the Toolbar at the top as well as the Scene/Game tabs right underneath.
Figure 1.11 Editor screenshot cropped to show Toolbar, Scene, and Game
工具栏左侧是用于场景导航和变换对象的按钮——用于环顾场景和移动对象。我建议你花时间练习这些,因为它们是你在 Unity 的可视化编辑器中要做的最重要的两项活动。(它们非常重要,因此在本节之后会有自己的章节。)
At the left side of the Toolbar are buttons for scene navigation and transforming objects—to look around the scene and to move objects. I suggest you spend time practicing these, because they are two of the most important activities you’ll do in Unity’s visual editor. (They’re so important that they get their own section following this one.)
工具栏右侧是布局和图层的下拉菜单。如前所述,Unity 界面的布局非常灵活,因此布局菜单允许您切换布局。至于图层菜单,这是您目前可以忽略的高级功能(图层将在以后提到)章节)。
The right-hand side of the Toolbar is where you’ll find drop-down menus for layouts and layers. As mentioned earlier, the layout of Unity’s interface is flexible, so the Layout menu allows you to switch layouts. As for the Layers menu, that’s advanced functionality that you can ignore for now (layers are mentioned in future chapters).
场景导航主要使用鼠标以及一些用于更改鼠标操作的修饰键来完成。三种主要的导航操作是移动、轨道和缩放。具体的鼠标移动取决于您使用的鼠标,并在附录 A 中进行了描述。这三种移动涉及单击和拖动,同时按住 Alt(或 Mac 上的 Option)和 Ctrl(Mac 上的 Command)。花几分钟在场景中移动,以了解移动、轨道和缩放的作用。
Scene navigation is primarily done using the mouse, along with a few modifier keys used to change what the mouse is doing. The three main navigation maneuvers are Move, Orbit, and Zoom. The specific mouse movements vary depending on the mouse you’re using and are described in appendix A. The three movements involve clicking and dragging while holding down a combination of Alt (or Option on Mac) and Ctrl (Command on a Mac). Spend a few minutes moving around in the scene to understand what Move, Orbit, and Zoom do.
提示尽管 Unity 可以使用一键或两键鼠标,但我强烈建议使用三键鼠标(是的,三键鼠标在 Mac 上也可以正常工作)。
TIP Although Unity can be used with one- or two-button mice, I highly recommend getting a three-button mouse (and yes, a three-button mouse works fine on a Mac).
物体的变换也是通过三个主要动作来完成的,三个场景导航动作类似于三个变换:平移、旋转和缩放(图 1.12 演示了立方体上的变换)。
Transforming objects is also done through three main maneuvers, and the three scene navigation moves are analogous to the three transforms: Translate, Rotate, and Scale (figure 1.12 demonstrates the transforms on a cube).
图 1.12 应用三种变换:平移、旋转和缩放。(较浅的线是对象在变换之前的先前状态。)
Figure 1.12 Applying the three transforms: Translate, Rotate, and Scale. (The lighter lines are the previous state of the object before it was transformed.)
当您选择场景中的对象时,您可以移动它(数学上准确的技术术语是平移)、旋转它和缩放它的大小。回到场景导航操作,移动对应于相机的平移,轨道对应于旋转,缩放对应于缩放。除了工具栏上的按钮外,您还可以通过按键盘上的 W、E 或 R 来切换这些功能。当您激活变换时,您会注意到场景中的对象上方出现了一组颜色编码的箭头或圆圈;这是变换小工具,您可以单击并拖动此小工具来应用变换。
When you select an object in the scene, you can then move it around (the mathematically accurate technical term is translate), rotate it, and scale its size. Relating back to scene navigation maneuvers, Move corresponds to Translate for the camera, Orbit corresponds to Rotate, and Zoom corresponds to Scale. Besides the buttons on the Toolbar, you can switch these functions by pressing W, E, or R on the keyboard. When you activate a transform, you’ll notice that a set of color-coded arrows or circles appears over the object in the scene; this is the Transform gizmo, and you can click and drag this gizmo to apply the transformation.
第四个工具位于变换按钮旁边。称为矩形工具,它是为 2D 图形设计的。这个工具结合了移动、旋转和缩放。同样,第五个按钮是结合了 3D 对象的移动、旋转和缩放的工具。就我个人而言,我更喜欢分别操作这三种变换,但您可能会发现组合工具更方便。
A fourth tool is next to the transform buttons. Called the Rect tool, it’s designed for use with 2D graphics. This one tool combines movement, rotation, and scaling. Similarly, the fifth button is for a tool that combines movement, rotation, and scaling for 3D objects. Personally, I prefer to manipulate the three transforms separately, but you may find the combined tools more convenient.
Unity 还有许多其他键盘快捷键,可用于加速各种任务。请参阅附录 A 了解它们。然后,继续阅读本指南的其余部分界面!
Unity has a host of other keyboard shortcuts for speeding up a variety of tasks. Refer to appendix A to learn about them. And with that, on to the remaining sections of the interface!
寻找在屏幕的两侧,左侧是“层次结构”选项卡,右侧是“检查器”选项卡(见图 1.13)。层次结构列出了场景中每个对象的名称,并根据它们在场景中的层次结构链接将名称嵌套在一起。基本上,这是一种按名称选择对象的方式,而不是在场景视图中搜索并单击它们。层次结构链接将对象直观地分组在一起,就像文件夹一样,允许您将整个组作为一个整体移动。
Looking at either side of the screen, you’ll see the Hierarchy tab on the left and the Inspector tab on the right (see figure 1.13). Hierarchy lists the name of every object in the scene and nests the names together according to their hierarchy linkages in the scene. Basically, it’s a way of selecting objects by name instead of hunting them down and clicking them within the Scene view. The Hierarchy linkages group objects together visually, like folders, allowing you to move the entire group as one.
Figure 1.13 Editor screenshot cropped to show the Hierarchy and Inspector tabs
检查器会显示当前选定对象的信息。选择一个对象,检查器就会显示有关该对象的信息。显示的信息基本上是组件列表,您甚至可以从对象中附加或删除组件。所有游戏对象都至少有一个组件,即 Transform,因此您始终会在检查器中看到有关定位和旋转的信息。通常,对象会在此处列出多个组件,包括附加到他们。
The Inspector shows you information about the currently selected object. Select an object, and the Inspector is then filled with information about that object. The information shown is pretty much a list of components, and you can even attach or remove components from objects. All game objects have at least one component, Transform, so you’ll always see at least information about positioning and rotation in the Inspector. Often, objects will have several components listed here, including scripts attached to them.
在在屏幕底部,您将看到项目和控制台(见图 1.14)。与场景和游戏一样,它们不是屏幕的两个独立部分,而是您可以在其间切换的选项卡。
At the bottom of the screen, you’ll see Project and Console (see figure 1.14). As with Scene and Game, these aren’t two separate portions of the screen, but rather tabs that you can switch between.
Project显示项目中的所有资产(艺术、代码等)。具体来说,视图左侧是项目目录的列表;当您选择一个目录时,视图右侧会显示该目录中的各个文件。Project 中的目录列表类似于 Hierarchy 中的列表视图,但 Hierarchy 显示场景中的对象;Project 显示可能不包含在任何特定场景中的文件(包括场景文件 - 当您保存场景时,它会显示在 Project 中!)。
Project shows all the assets (art, code, and so on) in the project. Specifically, on the left side of the view is a listing of the project’s directories; when you select a directory, the right side of the view shows the individual files in that directory. The directory listing in Project is similar to the list view in Hierarchy, but Hierarchy shows objects in the scene; Project shows files that may not be contained within any specific scene (including scene files—when you save a scene, it shows up in Project!).
Figure 1.14 Editor screenshot cropped to show the Project and Console tabs
提示Project 视图会镜像磁盘上的 Assets 目录,但通常情况下,您不应直接通过转到操作系统文件资源管理器中的 Assets 文件夹来移动或删除文件。如果您在 Project 视图中执行这些操作,Unity 将与该文件夹保持同步。
TIP Project view mirrors the Assets directory on disk, but generally, you shouldn’t move or delete files directly by going to the Assets folder in your OS’s file explorer. If you do those things within the Project view, Unity will keep in sync with that folder.
Console选项卡是显示代码消息的地方。其中一些消息是你故意放置的调试输出,但如果在脚本中遇到问题,Unity 也会发出错误消息你写道。
The Console tab is the place where messages from the code show up. Some of these messages will be debugging output that you placed deliberately, but Unity also emits error messages if it encounters problems in the script you wrote.
现在让我们看看 Unity 中的编程过程是如何进行的。虽然可以在可视化编辑器中布置艺术资产,但您需要编写代码来控制它们并使游戏具有交互性。Unity 中的复杂编程是使用 C# 作为编程语言完成的。
Now let’s look at how the process of programming works in Unity. Although art assets can be laid out in the visual editor, you need to write code to control them and make the game interactive. Complex programming in Unity is done using C# as the programming language.
启动 Unity 并创建一个新项目:在 Unity Hub 中选择“新建”,如果 Unity 已在运行,则选择“文件”>“新建项目”。输入项目名称,保留默认的 3D 模板(后续章节将提到 2D),然后选择要保存项目的位置。Unity 项目只是一个包含各种资产和设置文件的目录,因此请将项目保存在计算机上的任何位置。单击“创建”,然后 Unity 将在设置项目目录时短暂消失。
Launch Unity and create a new project: choose New in Unity Hub, or choose File > New Project if Unity is already running. Type a name for the project, leave the default 3D template (future chapters mention 2D), and then choose where you want to save the project. A Unity project is simply a directory full of various asset and settings files, so save the project anywhere on your computer. Click Create, and then Unity will briefly disappear while it sets up the project directory.
或者,您可以打开第 1 章的示例项目。我强烈建议您尝试在新项目中遵循接下来的说明,然后仅查看完成的示例以检查您的工作,但这取决于您。选择 Unity Hub 中的“添加”以将下载的项目文件夹添加到列表中,然后单击列表中的项目。
Alternatively, you could open the chapter 1 sample project. I strongly recommend you try to follow the upcoming instructions in a new project, and look at the finished sample only afterward to check your work, but it’s up to you. Choose Add in Unity Hub to add a downloaded project folder to the list and then click the project in the list.
警告如果您打开本书的示例项目而不是创建新项目, Unity可能会发出以下消息:重建 库, 因为 找不到资产 数据库!这指的是项目的库文件夹;该文件夹包含 Unity 生成并在工作时使用 的文件,但没有必要分发这些文件。
WARNING If you are opening the book’s sample project rather than creating a new project, Unity may emit the following message: Rebuilding Library because the asset database could not be found! This refers to the project’s Library folder; that folder contains files generated by Unity and used while working, but it is not necessary to distribute those files.
当 Unity 再次出现时,你会看到一个空白项目。接下来,让我们讨论一下程序如何在 Unity 中执行。
When Unity reappears, you’ll be looking at a blank project. Next, let’s discuss how programs get executed in Unity.
全部Unity 中的代码执行从链接到场景中对象的代码文件开始。最终,此代码执行都是前面描述的组件系统的一部分;游戏对象构建为组件集合,并且该集合可以包含要执行的脚本。
All code execution in Unity starts from code files linked to an object in the scene. Ultimately, this code execution is all part of the component system described earlier; game objects are built up as a collection of components, and that collection can include scripts to execute.
注意Unity 将代码文件称为脚本,使用在浏览器中运行 JavaScript 时最常遇到的脚本定义:代码在 Unity 游戏引擎中执行,而不是作为其自身可执行文件运行的编译代码。但不要混淆,因为许多人对这个词的定义不同;例如,脚本通常是指简短、独立的实用程序。Unity 中的脚本更类似于单个 OOP 类,而附加到场景中对象的脚本是对象实例。
NOTE Unity refers to the code files as scripts, using a definition of script that’s most commonly encountered with JavaScript running in a browser: the code is executed within the Unity game engine, as opposed to compiled code that runs as its own executable. But don’t get confused, because many people define the word differently; for example, scripts often refer to short, self-contained utility programs. Scripts in Unity are more akin to individual OOP classes, and scripts attached to objects in the scene are object instances.
您可能已经从此描述中推测出,在 Unity 中,脚本是组件 — 请注意,并非所有脚本都是组件,只有从脚本组件的基类 MonoBehaviour 继承的脚本才是组件。MonoBehaviour定义了将组件附加到游戏对象的不可见基础,并且(如清单 1.1 所示)从它继承提供了几个您可以实现的自动运行方法。这些方法包括Start(),在对象变为活动状态时调用一次(通常是在包含该对象的场景加载后立即调用),然后Update(),该方法每帧都会被调用。当你将代码放入这些预定义方法中时,代码就会运行。
As you’ve probably surmised from this description, in Unity, scripts are components—not all scripts, mind you, only scripts that inherit from MonoBehaviour, the base class for script components. MonoBehaviour defines the invisible groundwork for attaching components to game objects, and (as shown in listing 1.1) inheriting from it provides a couple of automatically run methods that you can implement. Those methods include Start(), called once when the object becomes active (which is generally as soon as the scene with that object has loaded), and Update(), which is called every frame. Your code is run when you put it inside these predefined methods.
定义框架是循环游戏代码的单个周期。几乎所有视频游戏(不仅在 Unity 中,而且在一般视频游戏中)都是围绕核心游戏循环构建的,其中代码在游戏运行时循环执行。每个循环都包括绘制屏幕 - 因此名称为帧(就像电影的一系列静态帧)。
DEFINITION A frame is a single cycle of the looping game code. Nearly all video games (not only in Unity, but video games in general) are built around a core game loop, where the code executes in a cycle while the game is running. Each cycle includes drawing the screen—hence the name frame (like the series of still frames of a movie).
Listing 1.1 Code template for a basic script component
使用 System.Collections; ❶ 使用 System.Collections.Generic; 使用 UnityEngine; 公共类 HelloWorld : MonoBehaviour { ❷ 无效开始(){ // 做一次某件事 ❸ } 无效更新(){ // 每帧执行某件事 ❹ } }
using System.Collections; ❶ using System.Collections.Generic; using UnityEngine; public class HelloWorld : MonoBehaviour { ❷ void Start() { // do something once ❸ } void Update() { // do something every frame ❹ } }
❶ Include namespaces for Unity and .NET/Mono classes.
❸ Put code here that runs once.
❹ Put code here that runs every frame.
这是您创建新的 C# 脚本时文件所包含的内容:定义有效 Unity 组件的最小样板代码。Unity 在应用程序内部隐藏了一个脚本模板,当您创建新脚本时,Unity 会复制该模板并重命名类以匹配文件的名称(在我的情况下为 HelloWorld.cs)。Unity 还为Start()和Update()提供了空壳,因为这两个地方是您调用自定义代码的最常见位置。
This is what the file contains when you create a new C# script: the minimal boilerplate code that defines a valid Unity component. Unity has a script template tucked away in the bowels of the application, and when you create a new script, Unity copies that template and renames the class to match the name of the file (which is HelloWorld.cs in my case). Unity also has empty shells for Start() and Update(), because those are the two most common places from which you’ll call your custom code.
要创建脚本,请从“创建”菜单中选择“C# 脚本”,您可以在“资产”菜单下访问该菜单(请注意,资产和游戏对象都有“创建”列表,但它们是不同的菜单),也可以在“项目”视图中单击鼠标右键。输入新脚本的名称,例如HelloWorld。如本章后面所述(见图 1.16),您将单击并拖动此脚本文件到场景中的对象上。双击该脚本,它将自动在另一个程序中打开以供编辑,如上所述下一个。
To create a script, select C# Script from the Create menu, which you access either under the Assets menu (note that Assets and GameObjects both have listings for Create, but they’re different menus) or by right-clicking in the Project view. Type in a name for the new script, such as HelloWorld. As explained later in the chapter (see figure 1.16), you’ll click and drag this script file onto an object in the scene. Double-click the script, and it’ll automatically be opened in another program for editing, as discussed next.
编程并非完全在 Unity 中完成,而是代码作为单独的文件存在,您可以将其指向 Unity。可以在 Unity 中创建脚本文件,但您仍需要使用文本编辑器或 IDE 在这些最初为空的文件中编写所有代码。Unity 附带 Microsoft Visual Studio,这是一个适用于 C# 的 IDE(图 1.15 显示了它的外观)。您可以访问https://visualstudio.microsoft.com了解有关此软件的更多信息。
Programming isn’t done within Unity exactly, but rather code exists as separate files that you point Unity to. Script files can be created within Unity, but you still need to use a text editor or IDE to write all the code within those initially empty files. Unity comes with Microsoft Visual Studio, an IDE for C# (figure 1.15 shows what it looks like). You can visit https://visualstudio.microsoft.com to learn more about this software.
Figure 1.15 Parts of the interface in Visual Studio
注意如果 Unity 打开的 IDE 与 Visual Studio 不同,您可能需要切换“外部工具”首选项。转到“首选项”>“外部工具”>“外部脚本编辑器”以选择一个 IDE。
NOTE If Unity opens a different IDE than Visual Studio, you may want to switch the External Tools preference. Go to Preferences > External Tools > External Script Editor to select an IDE.
注意Visual Studio 将文件组织成称为解决方案的组。Unity 会自动生成包含所有脚本文件的解决方案,因此您通常无需担心这一点。
NOTE Visual Studio organizes files into groupings called a solution. Unity automatically generates a solution that has all the script files, so you usually don’t need to worry about that.
有各种版本的 Visual Studio 可供选择(许多程序员更喜欢 Visual Studio Code),或者您可以使用完全不同公司的 IDE,例如 JetBrains Rider。切换到其他 IDE 非常简单,只需转到 Unity 偏好设置中的外部工具即可。我通常使用 Visual Studio for Mac,但您可以使用其他 IDE 而不会遇到任何问题。除了本章介绍之外,我不会谈论 IDE。
Various flavors of Visual Studio are available (many programmers prefer Visual Studio Code), or you could use an IDE from a completely different company, like JetBrains Rider. Switching to a different IDE is as simple as going to External Tools in Unity’s preferences. I generally use Visual Studio for Mac, but you could use a different IDE and not have any problems following along with this book. Beyond this introductory chapter, I’m not going to talk about the IDE.
请务必记住,尽管代码是在 Visual Studio 中编写的,但代码不会在那里运行。IDE 基本上是一个花哨的文本编辑器,当你在统一。
Always keep in mind that, although the code is written in Visual Studio, the code isn’t run there. The IDE is pretty much a fancy text editor, and the code is run when you click Play within Unity.
全部没错,项目中已经有一个空脚本,但您还需要场景中的一个对象来附加该脚本。回想一下图 1.1,它描述了组件系统的工作原理;脚本是一个组件,因此需要将其设置为对象上的组件之一。
All right, you already have an empty script in the project, but you also need an object in the scene to attach the script to. Recall figure 1.1 depicting how a component system works; a script is a component, so it needs to be set as one of the components on an object.
选择 GameObject > Create Empty,一个空白的 GameObject 就会出现在 Hierarchy 列表中。现在将脚本从 Project 视图拖到 Hierarchy 视图并将其放在空的 GameObject 上。如图 1.16 所示,Unity 将突出显示放置脚本的有效位置,将其放在 GameObject 上会将脚本附加到该对象上。
Choose GameObject > Create Empty, and a blank GameObject will appear in the Hierarchy list. Now drag the script from the Project view over to the Hierarchy view and drop it on the empty GameObject. As shown in figure 1.16, Unity will highlight valid places to drop the script, and dropping it on the GameObject will attach the script to that object.
Figure 1.16 How to link a script to a GameObject
要验证脚本是否已附加到对象,请选择 GameObject 并查看 Inspector 视图。您应该看到列出了两个组件:Transform 组件,它是所有对象都具有的基本位置/旋转/缩放组件,并且无法删除;其下方是您的脚本。
To verify that the script is attached to the object, select the GameObject and look at the Inspector view. You should see two components listed: the Transform component, which is the basic position/rotation/scale component all objects have and which can’t be removed, and below that, your script.
注意:最终,将对象从一个地方拖放到其他对象上这一动作会让人觉得很常规。Unity 中的许多链接(不仅仅是将脚本附加到对象)都是通过将对象拖到彼此之上来创建的。
NOTE Eventually, this action of dragging objects from one place and dropping them on other objects will feel routine. A lot of linkages in Unity, not only attaching scripts to objects, are created by dragging things on top of each other.
当脚本链接到对象时,您将看到类似图 1.17 的内容,其中脚本显示为 Inspector 中的一个组件。现在,当您播放场景时,脚本将执行,尽管由于您尚未编写任何代码,因此不会发生任何事情。接下来让我们这样做!
When a script is linked to an object, you’ll see something like figure 1.17, with the script showing up as a component in the Inspector. Now the script will execute when you play the scene, although nothing is going to happen yet because you haven’t written any code. Let’s do that next!
Figure 1.17 Linked script being displayed in the Inspector
双击打开脚本并返回到清单 1.1。学习新编程环境时,经典的起点是让它打印文本Hello World! ,因此在Start()方法中添加以下清单中的行。
Double-click the script to open it and get back to listing 1.1. The classic place to start when learning a new programming environment is having it print the text Hello World!, so add the line in the following listing inside the Start() method.
Listing 1.2 Adding a console message
...
无效开始(){
Debug.Log(“Hello World!”); ❶
}
......
void Start() {
Debug.Log("Hello World!"); ❶
}
...
❶ Add the logging command here.
Debug.Log( )命令在 Unity 中将消息打印到控制台视图。同时,该行进入Start()方法,因为如前所述,该方法在对象变为活动状态时立即被调用。单击编辑器中的“播放”后,将立即调用一次Start()。添加日志命令后,保存脚本,单击 Unity 中的“播放”,然后切换到控制台视图。您将看到消息Hello World!出现。恭喜您 - 您已经编写了第一个 Unity 脚本!当然,代码在后面的章节中会更加详细,但这是重要的第一步。
The Debug.Log() command prints a message to the Console view in Unity. Meanwhile, that line goes in the Start() method because, as was explained earlier, that method is called as soon as the object becomes active. Start() will be called once, as soon as you click Play in the editor. Once you’ve added the log command, save the script, click Play in Unity, and switch to the Console view. You’ll see the message Hello World! appear. Congratulations—you’ve written your first Unity script! Of course, the code will be more elaborate in later chapters, but this is an important first step.
警告:调整脚本后,请务必记住保存文件!一个很常见的错误是调整代码,然后立即单击 Unity 中的“播放”而不保存,导致游戏仍然使用调整前的代码。
WARNING Always remember to save the file after making adjustments to a script! A pretty common mistake is to adjust the code and then immediately click Play in Unity without saving, resulting in the game still using the code from before you adjusted it.
现在是时候保存场景了;这将创建一个带有 Unity 图标的 .unity 文件。场景文件是游戏中当前加载的所有内容的快照,以便您稍后可以重新加载此场景。保存这个场景似乎不值得,因为它非常简单(一个空的游戏对象)——但如果你不保存场景,当你在之后回到项目时,你会发现它又是空的退出统一。
Now it’s time to save the scene; this creates a .unity file with the Unity icon. The scene file is a snapshot of everything currently loaded in the game so that you can reload this scene later. Saving this scene may hardly seem worthwhile because it’s so simple (a single empty GameObject)—but if you don’t save the scene, you’ll find it empty again when you come back to the project after quitting Unity.
第 1 章以传统的“Hello World!”介绍新编程工具结束;现在是时候深入研究一个非平凡的 Unity 项目了,该项目具有交互性和图形功能。您将把对象放入场景中并编写代码以使玩家能够在该场景中行走。基本上,它将是没有怪物的 Doom(类似于图 2.1 中的描述)。Unity 中的可视化编辑器使新用户能够立即开始组装 3D 原型,而无需先编写大量样板代码(例如初始化 3D 视图或建立渲染循环)。
Chapter 1 concluded with the traditional “Hello World!” introduction to a new programming tool; now it’s time to dive into a nontrivial Unity project, a project with interactivity and graphics. You’ll put objects into a scene and write code to enable a player to walk around that scene. Basically, it’ll be Doom without the monsters (something like the depiction in figure 2.1). The visual editor in Unity enables new users to start assembling a 3D prototype right away, without needing to write a lot of boilerplate code first (for things like initializing a 3D view or establishing a rendering loop).
图 2.1 3D 演示的屏幕截图(基本上就是没有怪物的《毁灭战士》)
Figure 2.1 Screenshot of the 3D demo (basically, Doom without the monsters)
立即开始在 Unity 中构建场景是件很诱人的事情,尤其是对于这样一个简单(概念上!)的项目。但最好在开始时停下来,规划一下你要做什么,这一点现在尤其重要,因为你对这个过程还不熟悉。
It’s tempting to immediately start building the scene in Unity, especially with such a simple (in concept!) project. But it’s always a good idea to pause at the beginning and plan out what you’re going to do, and this is especially important right now because you’re new to the process.
注意:请记住,每章的项目都可以从本书的网站(http://mng.bz/VBY5)下载。首先在 Unity 中打开项目,然后打开主场景(通常简称为 Scene)进行运行和检查。在学习期间,我建议您自己输入所有代码,并仅将下载的示例用作参考。
NOTE Remember, the project for every chapter can be downloaded from the book’s website (http://mng.bz/VBY5). First open the project in Unity and then open the main scene (usually just named Scene) to run and inspect. While you’re learning, I recommend you type out all the code yourself and use the downloaded sample only for reference.
Unity 让新手可以轻松上手,但在您构建完整场景之前,我们先来了解一下几点。即使使用像 Unity 这样灵活的工具,您也需要了解自己要实现的目标。您还需要掌握 3D 坐标的运作方式,否则当您尝试在场景中定位对象时,您可能会迷失方向。
Unity makes it easy for a newcomer to get started, but let’s go over a couple of points before you build the complete scene. Even when working with a tool as flexible as Unity, you need to have a sense of the goal you’re working toward. You also need to grasp how 3D coordinates operate, or you could get lost as soon as you try to position an object in the scene.
前当你开始编程时,你总是想停下来问自己,“那么我在这里构建什么?”游戏设计是一个很大的话题,有许多令人印象深刻的厚书专注于如何设计游戏。幸运的是,对于我们的目的,你只需要记住这个简单演示的简要概述就可以开发一个基本的学习项目。这些初始项目无论如何都不会是非常复杂的设计,以避免分散你学习编程概念的注意力。在掌握游戏开发的基础知识后,你可以(也应该!)担心更高级别的设计问题。
Before you start programming anything, you always want to pause and ask yourself, “So what am I building here?” Game design is a huge topic, with many impressively large books focused on how to design a game. Fortunately, for our purposes, you need only a brief outline of this simple demo in mind to develop a basic learning project. These initial projects won’t be terribly complex designs anyway, in order to avoid distracting you from learning programming concepts. You can (and should!) worry about higher-level design issues after you’ve mastered the fundamentals of game development.
对于第一个项目,你将构建一个基本的第一人称射击游戏(FPS)场景。我们将创建一个房间来四处游走,玩家将从角色的角度看世界,并可以使用鼠标和键盘控制角色。现在可以剥离完整游戏的所有有趣复杂性,专注于核心机制:在 3D 空间中移动。图 2.2 描绘了这个项目的路线图,列出了我在脑海中构建的清单:
For this first project, you’ll build a basic first-person shooter (FPS) scene. We will create a room to navigate around, and players will see the world from their character’s point of view and can control the character by using the mouse and keyboard. All the interesting complexity of a complete game can be stripped away for now to concentrate on the core mechanics: moving around in a 3D space. Figure 2.2 depicts the road map for this project, laying out the checklist I built in my head:
Set up the room: create the floor, outer walls, and inner walls.
Create the player object (including attaching the camera on top).
Write movement scripts: rotate with the mouse and move with the keyboard.
Figure 2.2 Road map for the 3D demo
不要被这份路线图中的所有内容吓到!这章中的步骤听起来很多,但 Unity 让它们变得简单。接下来关于移动脚本的部分之所以如此广泛,只是因为我们将逐行介绍,以便您详细了解所有概念。
Don’t be scared off by everything in this road map! It sounds like a lot of steps in this chapter, but Unity makes them easy. The upcoming sections about movement scripts are so extensive only because we’ll be going through every line so that you can understand all the concepts in detail.
这个项目是第一人称演示,以便保持艺术要求简单;因为你看不到自己,所以“你”可以是一个圆柱形,顶部有一个摄像头!现在你需要理解如何3D 坐标的作用是,将所有内容放置在可视化编辑器中简单的。
This project is a first-person demo in order to keep the art requirements simple; because you can’t see yourself, it’s fine for “you” to be a cylindrical shape with a camera on top! Now you need to understand how 3D coordinates work so that placing everything in the visual editor will be easy.
如果想想我们开始使用的简单计划,它有三个方面:房间、视图和控件。所有这些项目都依赖于您对 3D 计算机模拟中位置和运动的表示方式的理解。如果您是 3D 图形方面的新手,您可能还不了解这些内容。
If you think about the simple plan we’re starting with, it has three aspects: a room, a view, and controls. All of these items rely on you understanding how positions and movements are represented in 3D computer simulations. If you’re new to working with 3D graphics, you might not already know this stuff.
归根结底,这一切都是指示空间点的数字,而这些数字与空间关联的方式是通过坐标轴。如果你回想一下数学课,你可能已经看到并使用 x 轴和 y 轴(见图 2.3)来为页面上的点分配坐标。这被称为笛卡尔坐标系。
It all boils down to numbers that indicate points in space, and the way those numbers correlate to the space is through coordinate axes. If you think back to math class, you’ve probably seen and used x- and y-axes (see figure 2.3) for assigning coordinates to points on the page. This is referred to as a Cartesian coordinate system.
Figure 2.3 Coordinates along the x- and y-axes define a 2D point.
两个轴为您提供 2D 坐标,所有点都在同一平面上。三个轴用于定义 3D 空间。由于 x 轴沿页面水平方向延伸,y 轴沿页面垂直延伸,我们现在想象第三个轴,它垂直于 x 轴和 y 轴,直入和直出页面。图 2.4 描绘了 3D 坐标空间的 x、y 和 z 轴。场景中具有特定位置的所有事物都将具有 x、y 和 z 坐标:玩家的位置、墙壁的位置等等。
Two axes give you 2D coordinates, with all points in the same plane. Three axes are used to define 3D space. Because the x-axis goes along the page horizontally and the y-axis goes along the page vertically, we now imagine a third axis that sticks straight into and out of the page, perpendicular to both the x- and y-axes. Figure 2.4 depicts the x-, y-, and z-axes for 3D coordinate space. Everything that has a specific position in the scene will have x-, y-, and z-coordinates: the position of the player, the placement of a wall, and so forth.
图 2.4 沿 x、y 和 z 轴的坐标定义一个 3D 点。
Figure 2.4 Coordinates along the x-, y-, and z-axes define a 3D point.
在 Unity 的 Scene 视图中,您可以看到这三个轴。在 Inspector 中,您可以输入定位对象所需的三个数字。您不仅可以编写代码来使用这三个数字坐标定位对象,还可以将移动定义为沿每个轴移动的距离。
In Unity’s Scene view, you can see these three axes displayed. In the Inspector, you can type in the three numbers required to position an object. You will not only write code to position objects using these three-number coordinates, but also define movements as a distance to move along each axis.
现在你已经对这个项目有了计划,并且知道如何使用坐标在 3D 空间中定位对象,现在是时候开始构建了現場。
Now that you have a plan in mind for this project and know how coordinates are used to position objects in 3D space, it’s time to start building the scene.
让我们在场景中创建和放置对象。首先,您将设置所有静态场景——地板和墙壁。然后,您将在场景周围放置灯光并定位相机。最后,您将创建将成为玩家的对象,您将向该对象附加脚本以在场景中行走。图 2.5 显示了一切就绪后编辑器的外观。
Let’s create and place objects in the scene. First, you’ll set up all the static scenery—the floor and walls. Then you’ll place lights around the scene and position the camera. Lastly, you’ll create the object that will be the player, the object to which you’ll attach scripts to walk around the scene. Figure 2.5 shows what the editor will look like with everything in place.
图 2.5 编辑器中的场景,包含地板、墙壁、灯光、摄像机和玩家
Figure 2.5 Scene in the editor with floor, walls, lights, a camera, and the player
第 1 章介绍了如何在 Unity 中创建新项目,因此您现在就来做这件事。在 Unity Hub 中选择“新建”(或在编辑器中选择“文件”>“新建项目”),然后在弹出的窗口中命名您的新项目。场景开始时大部分是空的,要创建的第一个对象是最明显的。
Chapter 1 showed how to create a new project in Unity, so you’ll do that now. Choose New in Unity Hub (or File > New Project in the editor) and then name your new project in the window that pops up. The scene starts out mostly empty, and the first objects to create are the most obvious ones.
选择屏幕顶部的 GameObject 菜单,然后将鼠标悬停在 3D Object 上以查看下拉菜单。选择 Cube 在场景中创建一个新的立方体对象(稍后,我们将使用其他形状,如球体和胶囊)。调整此对象的位置和比例以及其名称以制作地板。图 2.6 显示了应在 Inspector 中将地板设置为哪些值(在将其拉伸之前,它最初只是一个立方体)。
Select the GameObject menu at the top of the screen and then hover over 3D Object to see that drop-down menu. Select Cube to create a new cube object in the scene (later, we’ll use other shapes, like Sphere and Capsule). Adjust the position and scale of this object, as well as its name, to make the floor. Figure 2.6 shows which values the floor should be set to in the Inspector (it’s a cube only initially, before you stretch it out).
注意:您可以用任何您想要的单位来考虑位置的数字,只要您在整个场景中保持一致即可。最常见的单位是米,这也是我通常的选择,但有时我也使用英尺,我甚至看到其他人决定使用英寸作为数字!
NOTE You can think about the numbers for position in terms of any units you want, as long as you’re consistent throughout the scene. The most common choice for units is meters, and that’s what I generally choose, but I also use feet sometimes, and I’ve even seen other people decide that the numbers are inches!
重复相同的步骤来创建房间的外墙。您可以每次创建新的立方体,也可以使用标准快捷方式复制和粘贴现有对象。移动、旋转和缩放墙壁以形成地板周围的周界。尝试使用不同的数字(例如,缩放使用1、4、50 )或使用第 1.2.2 节中介绍的变换工具(请记住,在 3D 空间中移动和旋转的数学术语是变换)。
Repeat the same steps to create outer walls for the room. You can create new cubes each time, or you can copy and paste existing objects by using the standard shortcuts. Move, rotate, and scale the walls to form a perimeter around the floor. Experiment with different numbers (for example, 1, 4, 50 for Scale) or use the transform tools introduced in section 1.2.2 (remember that the mathematical term for moving and rotating in 3D space is transform).
提示调用第 1 章中的导航控件,从不同角度查看场景或缩小视图以鸟瞰。如果您在场景中迷失了方向,请按 F 键重置当前选定对象的视图。
TIP Recall the navigation controls in chapter 1 to view the scene from different angles or zoom out for a bird’s-eye view. If you ever get lost in the scene, press F to reset the view on the currently selected object.
Figure 2.6 Inspector view for the floor
外墙就位后,创建内墙以供四处走动。内墙的位置可随意调整;编写移动代码后,即可创建走廊和障碍物以供四处走动。墙壁最终的确切变换值将取决于您如何旋转和缩放立方体以适应,以及对象在层次结构视图中如何链接在一起。如果您需要示例来复制工作值,请下载示例项目并参考其中的墙壁。
Once the outer walls are in place, create inner walls to navigate around. Position the inner walls however you like; the idea is to create hallways and obstacles to walk around once you write code for movement. The exact Transform values that the walls end up with will vary depending on how you rotate and scale the cubes to fit, and on how the objects are linked together in the Hierarchy view. If you need an example to copy working values from, download the sample project and refer to the walls there.
提示:在层次结构视图中将对象拖放到彼此之上以建立链接。附加了伴随对象的对象称为父对象;附加到父对象的对象称为子对象。当移动(或旋转或缩放)父对象时,子对象会随之变换。
TIP Drag objects on top of each other in the Hierarchy view to establish linkages. Objects that have accompanying objects attached are referred to as parents; objects attached to parent objects are referred to as children. When the parent object is moved (or rotated or scaled), the child objects are transformed along with it.
定义一个根对象(与父对象和子对象的概念密切相关)是层次结构底部的对象,它本身没有父对象。因此,所有根对象都是父对象,但并非所有父对象都是根对象。
Definition A root object (closely related to the concepts of parent and child objects) is an object at the base of a hierarchy that does not itself have a parent. Thus, all root objects are parents, but not all parents are root objects.
您还可以创建空的游戏对象来组织场景。从 GameObject 菜单中,选择 Create Empty。通过将可见对象链接到根对象,可以折叠它们的层次结构列表。例如,在图 2.7 中,墙壁都是空根对象(名为 Building)的子对象,因此层次结构列表看起来井井有条。
You can also create empty game objects to use for organizing the scene. From the GameObject menu, choose Create Empty. By linking visible objects to a root object, their Hierarchy list can be collapsed. For example, in figure 2.7, the walls are all children of an empty root object (named Building) so that the Hierarchy list will look organized.
Figure 2.7 The Hierarchy view showing the walls and floor organized under an empty object
警告在将任何子对象链接到它之前,请确保重置空根对象的变换选项(位置和旋转为0、0、0以及比例为1、1、1 ),以避免子对象的位置出现任何异常。
Warning Before linking any child objects to it, make sure to reset the Transform options (Position and Rotation to 0, 0, 0 and Scale to 1, 1, 1) of the empty root object to avoid any oddities in the position of child objects.
如果还没有保存更改的场景,请记得保存。现在场景中有一个房间,但我们仍然需要设置灯光。让我们来处理一下下一个。
Remember to save the changed scene if you haven’t yet. Now the scene has a room in it, but we still need to set up the lighting. Let’s take care of that next.
通常,你用定向光照亮 3D 场景,然后进行一系列点光源。从定向光源开始。场景中可能已经默认有一个定向光源,但如果没有,请选择 GameObject > Light 并选择“定向光源”来创建一个。
Typically, you light a 3D scene with a directional light and then a series of point lights. Start with a directional light. The scene probably already has one by default, but if not, create one by choosing GameObject > Light and selecting Directional Light.
定向光的位置不会影响其投射的光线,只会影响光源所面对的方向,因此从技术上讲,您可以将该光放置在场景中的任何位置。我建议将定向光放置在房间上方,这样直观的感觉就像太阳一样,并且在您操纵场景的其余部分时不会挡住路。旋转此光并观察对房间的影响;我建议在 x 轴和 y 轴上稍微旋转它以获得良好的效果。
The position of a directional light doesn’t affect the light cast from it, only the direction the light source is facing, so technically, you could place that light anywhere in the scene. I recommend placing the directional light high above the room so that it intuitively feels like the sun and so that it’s out of the way when you’re manipulating the rest of the scene. Rotate this light and watch the effect on the room; I recommend rotating it slightly on both the x- and y-axes to get a good effect.
在检查器中,你会看到一个“强度”设置(见图 2.8)。顾名思义,该设置控制灯光的亮度。如果这是唯一的灯光,它必须更强烈,但由于你还会添加一堆点光源,因此这种定向光可能非常暗淡——例如,强度为0.6。这种光还应该有淡淡的黄色,就像太阳一样,而其他灯光将是白色的。
You will see an Intensity setting when you look in the Inspector (see figure 2.8). As the name indicates, that setting controls the brightness of the light. If this were the only light, it’d have to be more intense, but because you’ll add a bunch of point lights as well, this directional light can be pretty dim—for example, 0.6 Intensity. This light should also have a slight yellow tinge, like the sun, while the other lights will be white.
Figure 2.8 Directional light settings in the Inspector
至于点光源,使用相同的菜单创建多个点光源,并将它们放置在房间周围的暗处,以确保所有墙壁都被照亮。您不需要太多,因为如果游戏有大量灯光,性能可能会下降。在每个角落附近放置一个点光源应该没问题(我建议将它们提升到墙壁顶部),再加上一个放置在场景上方的高处(例如,Y 位置为 18),以给房间中的光线增添多样性。
As for point lights, create several by using the same menu and place them in dark spots around the room to make sure all the walls are lit. You don’t want too many, because performance can degrade if the game has lots of lights. Placing one near each corner should be fine (I suggest raising them to the tops of the walls), plus one placed high above the scene (for example, a Y position of 18) to give variety to the light in the room.
请注意,点光源在检查器中添加了“范围”设置(见图 2.9)。该设置控制光线照射的距离;而定向光源会均匀地照射整个场景,而物体越近,点光源就越亮。靠近地板的点光源的照射范围应为 18 左右,而放置在高处的光源的照射范围应为 40 左右,才能照射到整个房间。靠近地板的光源的强度应为0.8 ,而高处的光源的强度为0.4,用于填充空间的昏暗额外光线。
Note that point lights have a Range setting added to the Inspector (see figure 2.9). This controls how far away the light reaches; whereas directional lights cast light evenly throughout the entire scene, point lights are brighter when an object is closer. The point lights closer to the floor should have a range of around 18, but the light placed high up should have a range of around 40 to reach the entire room. Set Intensity to 0.8 for the lights closer to the floor, while the high one is dim extra light to fill the space, at intensity 0.4.
Figure 2.9 Point light settings in the Inspector
玩家看到场景所需的另一种对象是相机,但“空”场景带有主相机,因此您将使用它。如果您需要创建新相机(例如多人游戏中的分屏视图),相机是同一 GameObject 菜单中的另一个选择,如立方体和灯光。我们将相机定位在玩家的顶部,以便视图看起来像是通过玩家的眼睛。
The other kind of object needed for the player to see the scene is a camera, but the “empty” scene came with a main camera, so you’ll use that. If you ever need to create new cameras (such as for split-screen views in multiplayer games), Camera is another choice in the same GameObject menu, like Cube and Lights. We will position the camera around the top of the player so that the view appears to be through the player’s eyes.
为了在这个项目中,一个简单的原始形状就可以代表玩家。在 GameObject 菜单中(记住,将鼠标悬停在 3D Object 上以展开菜单),单击 Capsule。Unity 会创建一个两端圆润的圆柱形;这个原始形状将代表玩家。将此对象放置在 y 轴上的 1.1 处(对象高度的一半,再加一点以避免与地板重叠)。您可以沿 x 轴和 z 轴将对象移动到任何您想要的位置,只要它在房间内并且不接触任何墙壁即可。将对象命名为Player。
For this project, a simple primitive shape will do to represent the player. In the GameObject menu (remember, hover over 3D Object to expand the menu), click Capsule. Unity creates a cylindrical shape with rounded ends; this primitive shape will represent the player. Position this object at 1.1 on the y-axis (half the height of the object, plus a bit to avoid overlapping the floor). You can move the object along the x-axis and z-axis wherever you like, as long as it’s inside the room and not touching any walls. Name the object Player.
在检查器中,您会注意到此对象已分配一个胶囊碰撞器。这是胶囊对象的逻辑默认选择,就像立方体对象默认具有盒子碰撞器一样。但这个特定的对象将是玩家,因此需要与大多数对象略有不同的组件。通过单击该组件右上角的菜单图标来移除胶囊碰撞器,如图 2.10 所示;这将显示一个菜单,其中包含选项“移除组件”。碰撞器是围绕对象的绿色网格,因此删除胶囊碰撞器后,您会看到绿色网格消失。
In the Inspector, you’ll notice that this object has a capsule collider assigned to it. That’s a logical default choice for a capsule object, just as cube objects have a box collider by default. But this particular object will be the player and thus needs a slightly different sort of component than most objects. Remove the capsule collider by clicking the menu icon at the top right of that component, shown in figure 2.10; that will display a menu that includes the option Remove Component. The collider is a green mesh surrounding the object, so you’ll see the green mesh disappear after deleting the capsule collider.
Figure 2.10 Removing a component in the Inspector
我们将为该对象分配一个角色控制器,而不是胶囊对撞机。检查器底部有一个标记为“添加组件”的按钮;单击该按钮可打开可添加组件的菜单。在此菜单的“物理”部分中,您将找到“角色控制器”;选择该选项。顾名思义,此组件将允许对象像角色一样表现。
Instead of a capsule collider, we’re going to assign a character controller to this object. At the bottom of the Inspector is a button labeled Add Component; click that button to open a menu of components that you can add. In the Physics section of this menu, you’ll find Character Controller; select that option. As the name indicates, this component will allow the object to behave like a character.
您需要完成设置玩家对象的最后一步:连接摄像头。如前文第 2.2.1 节所述,可以在层次结构视图中将对象相互拖动。将将相机对象拖到玩家胶囊上,将相机连接到玩家。现在将相机定位到看起来像玩家的眼睛的位置(我建议将位置设置为0 , 0.5 , 0)。如有必要,将相机的旋转重置为0 , 0 , 0(如果您旋转了胶囊,则此项将关闭)。
You need to complete one last step to set up the player object: attaching the camera. As mentioned previously in section 2.2.1, objects can be dragged onto each other in the Hierarchy view. Drag the camera object onto the player capsule to attach the camera to the player. Now position the camera so that it’ll look like the player’s eyes (I suggest a Position of 0, 0.5, 0). If necessary, reset the camera’s Rotation to 0, 0, 0 (this will be off if you’ve rotated the capsule).
您已创建此场景所需的所有对象。剩下的就是编写代码来移动玩家目的。
You’ve created all the objects needed for this scene. What remains is writing code to move the player object.
到让玩家在场景中走动,你需要编写与玩家相关的移动脚本。请记住,组件是添加到对象的模块化功能,而脚本是一种组件。最终,这些脚本将响应键盘和鼠标输入,但首先你需要让玩家在原地旋转。
To have the player walk around the scene, you’ll write movement scripts attached to the player. Remember, components are modular bits of functionality that you add to objects, and scripts are a kind of component. Eventually, those scripts will respond to keyboard and mouse input, but first you’ll make the player spin in place.
这个简单的开始将教你如何在代码中应用变换。请记住,三种变换是平移、旋转和缩放;旋转物体意味着改变旋转。但关于这项任务,除了“这涉及旋转”之外,还有很多需要了解的内容。
This modest beginning will teach you how to apply transforms in code. Remember that the three transforms are Translate, Rotate, and Scale; spinning an object means changing the rotation. But there’s more to know about this task than only “this involves rotation.”
动画一个物体(例如让它旋转)归结为每帧移动一小段距离,帧反复播放。变换本身是即时应用的,而不是随着时间的推移而明显移动。但反复应用变换会导致物体看起来明显移动,就像翻页书中的一系列静止图画一样。图 2.11 说明了它的工作原理。
Animating an object (such as making it spin) boils down to moving it a small amount every frame, with the frames playing over and over. By themselves, transforms apply instantly, as opposed to visibly moving over time. But applying the transforms over and over causes the object to appear to visibly move, like a series of still drawings in a flip-book. Figure 2.11 illustrates how this works.
Figure 2.11 The appearance of movement: a cyclical process of transforming between still pictures
回想一下,脚本组件有一个Update()方法每帧运行一次。要旋转立方体,请在Update()中添加代码,使立方体稍微旋转一点。此代码将每帧反复运行。听起来很简单,正确的?
Recall that script components have an Update() method that runs every frame. To spin the cube, add code inside Update() that rotates the cube a small amount. This code will run over and over every frame. Sounds pretty simple, right?
现在让我们将刚刚讨论的概念付诸实践。创建一个新的 C# 脚本(记住,从 Assets 菜单中,打开 Create 子菜单),将其命名为Spin,并写入此代码(输入后不要忘记保存文件!)。
Now let’s put into action the concepts we’ve just discussed. Create a new C# script (remember, from the Assets menu, open the Create submenu), name it Spin, and write in this code (don’t forget to save the file after typing in it!).
Listing 2.1 Making the object spin
使用System.Collections; 使用 System.Collections.Generic; 使用 UnityEngine; ❶ 公共类 Spin:MonoBehaviour { 公共浮动速度 = 3.0f; ❷ 无效更新(){ 变换.旋转(0,速度,0); ❸ } }
using System.Collections; using System.Collections.Generic; using UnityEngine; ❶ public class Spin : MonoBehaviour { public float speed = 3.0f; ❷ void Update() { transform.Rotate(0, speed, 0); ❸ } }
❶ Pull Unity’s classes into this script.
❷ Declare a public variable for the speed of rotation.
❸ Put the Rotate command here so that it runs every frame.
要将脚本组件添加到玩家对象,请将脚本从“项目”视图向上拖放到“层次结构”视图中的“玩家”上。现在单击“播放”,您将看到视图旋转;您已经编写了使对象移动的代码!此代码几乎是新脚本的默认模板加上两行新添加的代码,所以让我们来看看这两行代码的作用。
To add the script component to the player object, drag the script up from the Project view and drop it onto Player in the Hierarchy view. Now click Play, and you’ll see the view spin around; you’ve written code to make an object move! This code is pretty much the default template for a new script plus two new added lines, so let’s examine what those two lines do.
首先,我们在类定义的顶部添加了速度变量(数字后面的f告诉计算机将其视为浮点值;否则,C# 将十进制数视为双精度数)。旋转速度被定义为变量而不是常量,因为 Unity 使用脚本组件中的公共变量做了一些方便的事情,如以下提示中所述。
First, we’ve added the variable for speed toward the top of the class definition (the f after the number tells the computer to treat this as a float value; otherwise, C# treats decimal numbers as a double). The rotation speed is defined as a variable rather than a constant because Unity does something handy with public variables in script components, as described in the following tip.
提示:公共变量在 Inspector 中公开,因此您可以在将组件添加到游戏对象后调整组件的值。这称为序列化值,因为 Unity 会保存变量的修改状态。
TIP Public variables are exposed in the Inspector so that you can adjust the component’s values after adding a component to a game object. This is referred to as serializing the value, because Unity saves the modified state of the variable.
图 2.12 显示了选择 Player 对象时 Inspector 中的组件的样子。您可以输入一个新数字,然后脚本将使用该值而不是代码中定义的默认值。这是一种方便的方法来调整不同对象上组件的设置,在可视化编辑器中工作,而不是对每个值进行硬编码。
Figure 2.12 shows what the component in the Inspector looks like when you select the Player object. You can type in a new number, and then the script will use that value instead of the default value defined in the code. This is a handy way to adjust settings for the component on different objects, working within the visual editor instead of hardcoding every value.
Figure 2.12 The Inspector displaying a public variable declared in the script
清单 2.1 中要检查的第二行是Rotate()方法。它位于Update()内部,因此命令每帧都会运行。Rotate ()是Transform类的一种方法,因此它通过此对象的 transform 组件以点符号调用(与大多数面向对象语言一样,如果您只输入transform ,则暗示使用this.transform)。该变换每帧旋转speed度,从而产生平滑的旋转运动。但为什么Rotate()的参数列为(0, speed, 0)而不是(speed, 0, 0)呢?
The second line to examine from listing 2.1 is the Rotate() method. That’s inside Update() so that the command runs every frame. Rotate() is a method of the Transform class, so it’s called with dot notation through the transform component of this object (as in most object-oriented languages, this.transform is implied if you type just transform). The transform is rotated by speed degrees every frame, resulting in a smooth spinning movement. But why are the parameters to Rotate() listed as (0, speed, 0) as opposed to, say, (speed, 0, 0)?
回想一下,三维空间中存在三个轴,分别标记为 x、y 和 z。理解这些轴与位置和运动的关系相当直观,但这些轴也可用于描述旋转。航空学以类似的方式描述旋转,因此使用 3D 图形的程序员经常使用一组借用自航空学的术语:俯仰、偏航和滚转。图 2.13 说明了这些术语的含义:俯仰是围绕 x 轴的旋转,偏航是围绕 y 轴的旋转,滚转是围绕 z 轴的旋转。
Recall that three axes exist in 3D space, labeled x, y, and z. Understanding how these axes relate to positions and movements is fairly intuitive, but these axes can also be used to describe rotations. Aeronautics describes rotations in a similar way, so programmers working with 3D graphics often use a set of terms borrowed from aeronautics: pitch, yaw, and roll. Figure 2.13 illustrates what these terms mean: pitch is rotation around the x-axis, yaw is rotation around the y-axis, and roll is rotation around the z-axis.
Figure 2.13 Illustration of pitch, yaw, and roll rotation of an aircraft
鉴于我们可以描述围绕 x、y 和 z 轴的旋转,这意味着Rotate()的三个参数是 X、Y 和 Z 旋转。因为我们希望玩家只向侧面旋转,而不是上下倾斜,所以应该只为 Y 旋转提供一个数字,为 X 和 Z 旋转提供一个 0。
Given that we can describe rotations around the x-, y-, and z-axes, that means the three parameters for Rotate() are X, Y, and Z rotation. Because we want the player to only spin around sideways, as opposed to tilting up and down, a number should be given for only the Y rotation, and 0 for X and Z rotation.
希望您能猜出如果将参数更改为(speed, 0, 0)然后播放场景会发生什么。现在就试试吧!接下来,您需要了解有关旋转和 3D 坐标轴的另一个微妙之处,体现在Rotate()的可选第四个参数中 方法。
Hopefully, you can guess what will happen if you change the parameters to (speed, 0, 0) and then play the scene. Try that now! Next, you need to understand one other subtle point about rotations and 3D coordinate axes, embodied in an optional fourth parameter to the Rotate() method.
经过默认情况下,Rotate()方法在本地坐标上运行。您可以使用的另一种坐标是全局坐标。您可以使用可选的第四个参数并写入Space.Self或Space.World来告诉该方法是使用本地坐标还是全局坐标,如下所示:Rotate(0, speed, 0, Space.World)。
By default, the Rotate() method operates on local coordinates. The other kind of coordinates you could use are global. You tell the method whether to use local or global coordinates by using an optional fourth parameter and writing either Space.Self or Space.World, like so: Rotate(0, speed, 0, Space.World).
参考 2.1.2 节关于三维坐标空间的解释,思考这些问题:(0, 0, 0) 在哪里?x 轴指向哪个方向?坐标系本身可以移动吗?
Refer to the explanation about 3D coordinate space in section 2.1.2 and ponder these questions: Where is (0, 0, 0) located? Which direction is the x-axis pointing in? Can the coordinate system itself move around?
原来,每一个物体都有自己的原点,以及三个轴的方向,并且这个坐标系会随着物体一起移动。这被称为局部坐标。整个 3D 场景也有自己的原点和三个轴的方向,并且这个坐标系永远不会移动。这被称为全局坐标。因此,当您为Rotate()方法指定局部或全局时,您就是在告诉它要围绕谁的 x、y 和 z 轴旋转(见图 2.14)。
It turns out that every single object has its own origin point, as well as its own direction for the three axes, and this coordinate system moves around with the object. This is referred to as local coordinates. The overall 3D scene also has its own origin point and its own direction for the three axes, and this coordinate system never moves. This is referred to as global coordinates. Therefore, when you specify local or global for the Rotate() method, you’re telling it whose x-, y-, and z-axes to rotate around (see figure 2.14).
Figure 2.14 Local versus global coordinate axes
如果您是 3D 图形的新手,那么这个概念可能有点难以理解。图 2.14 中描述了不同的轴(请注意,平面的“左”方向与世界的“左”方向是不同的),但理解局部和全局的最简单方法是通过示例。
If you’re new to 3D graphics, this is somewhat of a mind-bending concept. The different axes are depicted in figure 2.14 (notice how “left” to the plane is a different direction than “left” to the world), but the easiest way to understand local and global is through an example.
选择玩家对象,然后稍微倾斜它(X 轴旋转大约30 度)。这将偏离局部坐标,使局部和全局旋转看起来不同。现在尝试运行Spin脚本参数中添加和不添加Space.World时均如此。如果您难以想象发生了什么,请尝试从玩家对象中移除旋转组件,而是旋转放置在玩家面前的倾斜立方体。当您将命令设置为本地或全球的坐标。
Select the player object and then tilt it a bit (something like 30 for the X rotation). This will throw off the local coordinates so that local and global rotations look different. Now try running the Spin script both with and without Space.World added to the parameters. If it’s too hard for you to visualize what’s happening, try removing the spin component from the player object and instead spin a tilted cube placed in front of the player. You’ll see the object rotating around different axes when you set the command to local or global coordinates.
现在您将使旋转响应鼠标输入(即,此脚本所附加到的对象的旋转,在本例中为玩家)。您将分几个步骤完成此操作,逐步为角色添加新的移动能力。首先,玩家将只能左右旋转,然后玩家将只能上下旋转。最终,玩家将能够环顾四周(同时水平和垂直旋转),这种行为称为鼠标查看。
Now you’ll make rotation respond to input from the mouse (that is, rotation of the object this script is attached to, which in this case will be the player). You’ll do this in several steps, progressively adding new movement abilities to the character. First, the player will rotate only side to side, and then the player will rotate only up and down. Eventually, the player will be able to look around in all directions (rotating horizontally and vertically at the same time), a behavior referred to as mouse-look.
鉴于我们将使用三种类型的旋转行为(水平、垂直和两者),您将首先编写支持这三种行为的框架。创建一个新的 C# 脚本,将其命名为MouseLook,然后写入此代码。
Given that we will use three types of rotation behavior (horizontal, vertical, and both), you’ll start by writing the framework for supporting all three. Create a new C# script, name it MouseLook, and write in this code.
Listing 2.2 MouseLook framework with enum for the Rotation setting
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 MouseLook : MonoBehaviour {
公共枚举 RotationAxes { ❶
鼠标X和Y = 0,
鼠标X = 1,
鼠标 Y = 2
}
公共旋转轴轴=旋转轴.MouseXAndY; ❷
无效更新(){
如果 (axes == RotationAxes.MouseX) {
// 此处水平旋转 ❸
}
否则,如果(轴 == RotationAxes.MouseY){
// 此处垂直旋转 ❹
}
别的 {
// 这里同时进行水平和垂直旋转 ❺
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MouseLook : MonoBehaviour {
public enum RotationAxes { ❶
MouseXAndY = 0,
MouseX = 1,
MouseY = 2
}
public RotationAxes axes = RotationAxes.MouseXAndY; ❷
void Update() {
if (axes == RotationAxes.MouseX) {
// horizontal rotation here ❸
}
else if (axes == RotationAxes.MouseY) {
// vertical rotation here ❹
}
else {
// both horizontal and vertical rotation here ❺
}
}
}
❶ Define an enum data structure to associate names with settings.
❷ Declare a public variable to set in Unity’s editor.
❸ Put code here for horizontal rotation only.
❹ Put code here for vertical rotation only.
❺ Put code here for both horizontal and vertical rotation.
请注意,枚举用于为MouseLook脚本选择水平或垂直旋转。定义枚举数据结构允许您按名称设置值,而不是输入数字并试图记住每个数字的含义(0是水平旋转吗?是1吗?)。如果您随后声明一个类型为该枚举的公共变量,它将在 Inspector 中显示为下拉菜单(参见图 2.15),这对于选择设置很有用。
Notice that an enum is used to choose horizontal or vertical rotation for the MouseLook script. Defining an enum data structure allows you to set values by name, rather than typing in numbers and trying to remember what each number means (is 0 horizontal rotation? Is it 1?). If you then declare a public variable typed to that enum, it will display in the Inspector as a drop-down menu (see figure 2.15), which is useful for selecting settings.
Figure 2.15 The Inspector displays public enum variables as a drop-down menu.
删除Spin组件(与之前删除玩家胶囊的方式相同,使用右上角的菜单),然后将此新脚本附加到玩家对象。使用 Inspector 中的 Axes 下拉菜单切换旋转方向。设置好水平/垂直旋转后,您可以为条件语句的每个分支填写代码。
Remove the Spin component (the same way you removed the player’s capsule earlier, using the menu at the top right) and attach this new script to the player object instead. Use the Axes drop-down menu in the Inspector to switch the direction of rotation. With the horizontal/vertical rotation setting in place, you can fill in code for each branch of the conditional statement.
警告:在更改此轴的菜单设置之前,请务必停止游戏。Unity 允许您在游戏过程中编辑检查器(以测试设置更改),但在停止游戏后会恢复更改。
WARNING Make sure to stop the game before changing the menu setting for this axis. Unity allows you to edit the Inspector during the game (to test settings changes) but then reverts the change after you stop the game.
这第一个也是最简单的分支是水平旋转。首先编写与清单 2.1 中相同的旋转命令,以使对象旋转。不要忘记声明一个公共变量来表示旋转速度;在axis之后但在Update()之前声明新变量,并将变量命名为sensitiveHor,因为在涉及多个旋转后, speed这个名称太通用了。这次将变量的值增加到9,因为对于接下来几个清单中编写的代码,该值需要更大。调整后的代码应类似于此清单。
The first and simplest branch is for horizontal rotation. Start by writing the same rotation command you used in listing 2.1 to make the object spin. Don’t forget to declare a public variable for the rotation speed; declare the new variable after axes but before Update(), and call the variable sensitivityHor because speed is too generic a name after you have multiple rotations involved. Increase the value of the variable to 9 this time, because that value needs to be bigger for the code written over the next couple of listings. The adjusted code should look like this listing.
Listing 2.3 Horizontal rotation, not yet responding to the mouse
... 公共RotationAxes轴=RotationAxes.MouseXAndY; ❶ 公共浮点灵敏度Hor=9.0f; ❷ 无效更新(){ 如果 (axes == RotationAxes.MouseX) { transform.Rotate(0,sensitiveHor, 0); ❸ } ...
... public RotationAxes axes = RotationAxes.MouseXAndY; ❶ public float sensitivityHor = 9.0f; ❷ void Update() { if (axes == RotationAxes.MouseX) { transform.Rotate(0, sensitivityHor, 0); ❸ } ...
❶ Italicized code was already in script; it’s shown here for reference.
❷ Declare a variable for the speed of rotation.
❸ Put the Rotate command here so that it runs every frame.
将MouseLook组件的 Axes 菜单设置为水平旋转并播放脚本;视图将像以前一样旋转。下一步是让旋转对鼠标移动做出反应,因此让我们引入一个新方法:Input.GetAxis()。Input类有一堆用于处理输入设备(例如鼠标)的方法,以及GetAxis()方法返回与鼠标移动相关的数字(正 1 到 -1,取决于移动方向)。GetAxis ()将所需轴的名称作为参数,水平轴称为Mouse X。
Set the Axes menu of the MouseLook component to horizontal rotation and play the script; the view will spin as before. The next step is to make the rotation react to mouse movement, so let’s introduce a new method: Input.GetAxis(). The Input class has a bunch of methods for handling input devices (such as the mouse), and the GetAxis() method returns numbers correlated to the movement of the mouse (positive 1 to -1, depending on the direction of movement). GetAxis() takes the name of the axis desired as a parameter, and the horizontal axis is called Mouse X.
如果将旋转速度乘以轴值,则旋转将响应鼠标移动。速度将根据鼠标移动缩放,缩小到零甚至反转方向。旋转命令现在看起来像下面的清单。
If you multiply the rotation speed by the axis value, the rotation will respond to mouse movement. The speed will scale according to mouse movement, scaling down to zero or even reversing direction. The Rotate command now looks like the following listing.
Listing 2.4 Rotate command adjusted to respond to the mouse
...
transform.Rotate(0,Input.GetAxis("鼠标X") *sensitiveHor,0); ❶
......
transform.Rotate(0, Input.GetAxis("Mouse X") * sensitivityHor, 0); ❶
...
❶ Note the use of GetAxis() to get mouse input.
警告确保在Mouse X中输入空格。此命令的轴名称由 Unity 定义,而不是我们代码中的轴名称。为此轴输入MouseX是一个常见错误。
WARNING Make sure to type a space in Mouse X. The axis names for this command are defined by Unity, not the axis names from our code. Typing MouseX for this axis is a common mistake.
点击播放,然后移动鼠标。当你左右移动鼠标时,视图也会左右旋转。这太酷了!下一步是垂直旋转,而不是水平。
Click Play and then move the mouse around. As you move the mouse from side to side, the view will rotate from side to side. That’s pretty cool! The next step is to rotate vertically instead of horizontally.
为了水平旋转,我们一直在使用Rotate()方法,但对于垂直旋转,我们将采用不同的方法。虽然该方法对于应用变换很方便,但也有点不灵活。它只适用于无限制地增加旋转,这对于水平旋转来说很好,但垂直旋转需要限制视图可以向上或向下倾斜的程度。此清单显示了MouseLook的垂直旋转代码;随后将对代码进行详细解释。
For horizontal rotation, we’ve been using the Rotate() method, but we’ll take a different approach with vertical rotation. Although that method is convenient for applying transforms, it’s also kind of inflexible. It’s useful only for incrementing the rotation without limit, which was fine for horizontal rotation, but vertical rotation needs limits on how much the view can tilt up or down. This listing shows the vertical rotation code for MouseLook; a detailed explanation of the code will immediately follow.
Listing 2.5 Vertical rotation for MouseLook
... 公共浮动敏感度Hor = 9.0f; 公共浮动敏感度Vert = 9.0f; ❶ 公共浮点最小值Vert = -45.0f; 公共浮点最大垂直度 = 45.0f; 私有浮点垂直旋转 = 0; ❷ 无效更新(){ 如果 (axes == RotationAxes.MouseX) { transform.Rotate(0,Input.GetAxis("鼠标 X") *sensitiveHor,0); } 否则,如果(轴 == RotationAxes.MouseY){ verticalRot -= Input.GetAxis("鼠标 Y") *sensitiveVert; ❸ verticalRot = Mathf.Clamp(verticalRot, minimumVert, maximumVert); ❹ float HorizontalRot = transform.localEulerAngles.y; ❺ transform.localEulerAngles = new Vector3(verticalRot, HorizontalRot, 0); ❻ } ...
... public float sensitivityHor = 9.0f; public float sensitivityVert = 9.0f; ❶ public float minimumVert = -45.0f; public float maximumVert = 45.0f; private float verticalRot = 0; ❷ void Update() { if (axes == RotationAxes.MouseX) { transform.Rotate(0, Input.GetAxis("Mouse X") * sensitivityHor, 0); } else if (axes == RotationAxes.MouseY) { verticalRot -= Input.GetAxis("Mouse Y") * sensitivityVert; ❸ verticalRot = Mathf.Clamp(verticalRot, minimumVert, maximumVert); ❹ float horizontalRot = transform.localEulerAngles.y; ❺ transform.localEulerAngles = new Vector3(verticalRot, horizontalRot, 0); ❻ } ...
❶ Declare variables used for vertical rotation.
❷ Declare a private variable for the vertical angle.
❸ Increment the vertical angle based on the mouse.
❹ Clamp the vertical angle between minimum and maximum limits.
❺ Keep the same Y angle (i.e., no horizontal rotation).
❻ Create a new vector from the stored rotation values.
将MouseLook组件的 Axes 菜单设置为垂直旋转并播放新脚本。现在视图不会横向旋转,但会随着鼠标的上下移动而上下倾斜。倾斜停止在上限和下限。
Set the Axes menu of the MouseLook component to vertical rotation and play the new script. Now the view won’t rotate sideways but will tilt up and down when you move the mouse up and down. The tilt stops at upper and lower limits.
这段代码引入了几个需要解释的新概念。首先,我们这次不使用Rotate() ,因此我们需要一个变量来存储旋转角度(此变量在此处称为verticalRot,请记住垂直旋转围绕 x 轴进行)。Rotate ()方法会增加当前旋转,而这段代码直接设置旋转角度。这是“将角度加 5”和“将角度设置为 30”之间的区别。我们仍然需要增加旋转角度,但这就是代码具有-=运算符的原因:从旋转角度中减去一个值,而不是将角度设置为该值。通过不使用Rotate(),我们可以通过多种方式操纵旋转角度,而不仅仅是增加它。旋转值乘以Input.GetAxis(),就像水平旋转的代码一样,但现在我们要求的是鼠标 Y,因为这是鼠标的垂直轴。
This code introduces several new concepts that need to be explained. First off, we’re not using Rotate() this time, so we need a variable in which to store the rotation angle (this variable is called verticalRot here, and remember that vertical rotation goes around the x-axis). The Rotate() method increments the current rotation, whereas this code sets the rotation angle directly. It’s the difference between saying “add 5 to the angle” and “set the angle to 30.” We do still need to increment the rotation angle, but that’s why the code has the -= operator: to subtract a value from the rotation angle, rather than set the angle to that value. By not using Rotate(), we can manipulate the rotation angle in various ways aside from only incrementing it. The rotation value is multiplied by Input.GetAxis(), as in the code for horizontal rotation, except now we ask for Mouse Y because that’s the vertical axis of the mouse.
下一行将进一步操纵旋转角度。我们使用Mathf.Clamp()将旋转角度保持在最小和最大限度之间。这些限制是代码中先前声明的公共变量,它们确保视图只能向上或向下倾斜 45 度。Clamp ()方法并非特定于旋转,但通常用于在限制之间保持数字变量。要查看会发生什么,请尝试注释掉Clamp()行;现在倾斜不会在上限和下限处停止,甚至允许您完全上下颠倒地旋转!显然,颠倒地观察世界是不可取的,因此存在限制。
The rotation angle is manipulated further on the next line. We use Mathf.Clamp() to keep the rotation angle between minimum and maximum limits. Those limits are public variables declared earlier in the code, and they ensure that the view can tilt only 45 degrees up or down. The Clamp() method isn’t specific to rotation but is generally useful for keeping a number variable between limits. To see what happens, try commenting out the Clamp() line; now the tilt doesn’t stop at upper and lower limits, allowing you to even rotate completely upside down! Clearly, viewing the world upside down is undesirable, hence the limits.
因为transform的 angles 属性是Vector3,所以我们需要创建一个新的Vector3 ,并将旋转角度传递给构造函数。Rotate()方法为我们自动执行了这个过程,增加旋转角度,然后创建一个新的向量。
Because the angles property of transform is a Vector3, we need to create a new Vector3 with the rotation angle passed in to the constructor. The Rotate() method was automating this process for us, incrementing the rotation angle and then creating a new vector.
定义向量是将多个数字存储在一起作为一个单元。例如,Vector3是三个数字(标记为 x、y、z)。
DEFINITION A vector is multiple numbers stored together as a unit. For example, a Vector3 is three numbers (labeled x, y, z).
警告我们需要创建一个新的Vector3而不是更改变换中现有向量的值,原因是这些值对于变换来说是只读的。这是一个常见的错误,可能会让您犯错。
WARNING The reason we need to create a new Vector3 instead of changing values in the existing vector in the transform is that those values are read-only for transforms. This is a common mistake that can trip you up.
MouseLook的另一个旋转设置需要代码:同时进行水平和垂直旋转时间。
One more rotation setting for MouseLook needs code: horizontal and vertical rotation at the same time.
这最后一段代码也不会使用Rotate(),原因相同:垂直旋转角度在增加后被限制在限值之间。这意味着现在需要直接计算水平旋转。请记住,Rotate()正在自动执行增加旋转角度的过程,如下所示。
This last chunk of code won’t use Rotate() either, for the same reason: the vertical rotation angle is clamped between limits after being incremented. That means the horizontal rotation needs to be calculated directly now. Remember, Rotate() was automating the process of incrementing the rotation angle, shown here.
Listing 2.6 Horizontal and vertical MouseLook
...
别的 {
verticalRot -= Input.GetAxis("鼠标 Y") *sensitiveVert;
verticalRot = Mathf.Clamp(verticalRot, minimumVert, maximumVert);
float delta = Input.GetAxis("鼠标 X") *sensitiveHor; ❶
float HorizontalRot = transform.localEulerAngles.y + delta; ❷
变换.localEulerAngles = 新 Vector3(verticalRot,horizontalRot,0);
}
......
else {
verticalRot -= Input.GetAxis("Mouse Y") * sensitivityVert;
verticalRot = Mathf.Clamp(verticalRot, minimumVert, maximumVert);
float delta = Input.GetAxis("Mouse X") * sensitivityHor; ❶
float horizontalRot = transform.localEulerAngles.y + delta; ❷
transform.localEulerAngles = new Vector3(verticalRot, horizontalRot, 0);
}
...
❶ delta is the amount to change the rotation by.
❷ Increment the rotation angle by delta.
前几行处理verticalRot,与清单 2.5 中的完全相同。请记住,围绕对象的 x 轴旋转是垂直旋转。因为不再使用Rotate()方法处理水平旋转,所以delta和Horizo ntalRot行正在执行该操作。Delta是变化量的常用数学术语,因此我们对 delta 的计算是旋转应改变的量。然后将该变化量添加到当前旋转角度以获得所需的新旋转角度。
The first couple of lines, dealing with verticalRot, are exactly the same as in listing 2.5. Remember that rotating around the object’s x-axis is vertical rotation. Because horizontal rotation is no longer being handled using the Rotate() method, that’s what the delta and horizontalRot lines are doing. Delta is a common mathematical term for the amount of change, so our calculation of delta is the amount that rotation should change. That amount of change is then added to the current rotation angle to get the desired new rotation angle.
最后,垂直和水平两个角度都用于创建一个新向量,并将其分配给变换组件的角度属性。
Finally, both angles, vertical and horizontal, are used to create a new vector that’s assigned to the transform component’s angle property.
如果您不知道在哪里进行我们讨论过的各种更改和添加,此列表有完整的完成脚本。或者,下载示例项目。
In case you’ve gotten lost on where to make the various changes and additions we’ve gone over, this listing has the full finished script. Alternatively, download the example project.
Listing 2.7 The finished MouseLook script
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 MouseLook : MonoBehaviour {
公共枚举旋转轴 {
鼠标X和Y = 0,
鼠标X = 1,
鼠标 Y = 2
}
公共旋转轴轴=旋转轴.鼠标XAndY;
公共浮动敏感度Hor = 9.0f;
公共浮动敏感度Vert = 9.0f;
公共浮点最小值Vert = -45.0f;
公共浮点最大垂直度 = 45.0f;
私有浮点垂直旋转 = 0;
无效开始(){
Rigidbody body = GetComponent<Rigidbody>();
如果 (主体 != 空) {
身体.冻结旋转 = true;
}
}
无效更新(){
如果 (axes == RotationAxes.MouseX) {
transform.Rotate(0,Input.GetAxis("鼠标 X") *sensitiveHor,0);
}
否则,如果(轴 == RotationAxes.MouseY){
verticalRot -= Input.GetAxis("鼠标 Y") *sensitiveVert;
verticalRot = Mathf.Clamp(verticalRot, minimumVert, maximumVert);
浮动horizontalRot = 变换.localEulerAngles.y;
变换.localEulerAngles = 新 Vector3(verticalRot,horizontalRot,0);
}
别的 {
verticalRot -= Input.GetAxis("鼠标 Y") *sensitiveVert;
verticalRot = Mathf.Clamp(verticalRot, minimumVert, maximumVert);
float delta = Input.GetAxis("鼠标 X") *sensitiveHor;
浮动horizontalRot = 变换.localEulerAngles.y + delta;
变换.localEulerAngles = 新 Vector3(verticalRot,horizontalRot,0);
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MouseLook : MonoBehaviour {
public enum RotationAxes {
MouseXAndY = 0,
MouseX = 1,
MouseY = 2
}
public RotationAxes axes = RotationAxes.MouseXAndY;
public float sensitivityHor = 9.0f;
public float sensitivityVert = 9.0f;
public float minimumVert = -45.0f;
public float maximumVert = 45.0f;
private float verticalRot = 0;
void Start() {
Rigidbody body = GetComponent<Rigidbody>();
if (body != null) {
body.freezeRotation = true;
}
}
void Update() {
if (axes == RotationAxes.MouseX) {
transform.Rotate(0, Input.GetAxis("Mouse X") * sensitivityHor, 0);
}
else if (axes == RotationAxes.MouseY) {
verticalRot -= Input.GetAxis("Mouse Y") * sensitivityVert;
verticalRot = Mathf.Clamp(verticalRot, minimumVert, maximumVert);
float horizontalRot = transform.localEulerAngles.y;
transform.localEulerAngles = new Vector3(verticalRot, horizontalRot, 0);
}
else {
verticalRot -= Input.GetAxis("Mouse Y") * sensitivityVert;
verticalRot = Mathf.Clamp(verticalRot, minimumVert, maximumVert);
float delta = Input.GetAxis("Mouse X") * sensitivityHor;
float horizontalRot = transform.localEulerAngles.y + delta;
transform.localEulerAngles = new Vector3(verticalRot, horizontalRot, 0);
}
}
}
设置 Axes 菜单并运行新代码后,您可以在移动鼠标的同时环顾四周。太棒了!但您仍然被困在一个地方,环顾四周,就像安装在炮塔上一样。下一步是四处移动这场景。
When you set the Axes menu and run the new code, you’re able to look around in all directions while moving the mouse. Great! But you’re still stuck in one place, looking around as if mounted on a turret. The next step is moving around the scene.
寻找响应鼠标输入而移动是第一人称控制的重要组成部分,但您只做到了一半。玩家还需要响应键盘输入而移动。让我们编写一个键盘控制组件来补充鼠标控制组件;创建一个名为FPSInput的新 C# 脚本并将其附加到玩家(与MouseLook脚本一起)。目前,将MouseLook组件设置为仅水平旋转。
Looking around in response to mouse input is an important part of first-person controls, but you’re only halfway there. The player also needs to move in response to keyboard input. Let’s write a keyboard control component to complement the mouse control component; create a new C# script called FPSInput and attach that to the player (alongside the MouseLook script). For the moment, set the MouseLook component to horizontal rotation only.
提示这里介绍的键盘和鼠标控制被分成了单独的脚本。您不必以这种方式构造代码,可以将所有内容捆绑到单个玩家控制脚本中。但是,组件系统(例如 Unity 中的组件系统)往往最灵活,因此当您将功能分成几个较小的组件时最有用。
TIP The keyboard and mouse controls explained here are split into separate scripts. You don’t have to structure the code this way and could have everything bundled into a single player control script. But a component system (such as the one in Unity) tends to be most flexible and therefore most useful when you have functionality split into several smaller components.
您在上一节中编写的代码仅影响旋转,但现在我们将改变对象的位置。请参阅清单 2.1;将其输入到FPSInput中,但将Rotate()更改为Translate()。当您单击“播放”时,视图会向上滑动而不是旋转。
The code you wrote in the previous section affected rotation only, but now we’ll change the object’s position instead. Refer to listing 2.1; type that into FPSInput, but change Rotate() to Translate(). When you click Play, the view slides up instead of spinning around.
尝试更改参数值,看看移动如何变化(特别是尝试交换第一个和第二个数字)。经过一段时间的试验后,您可以继续添加键盘输入。
Try changing the parameter values to see how the movement changes (in particular, try swapping the first and second numbers). After experimenting with that for a bit, you can move on to adding keyboard input.
清单 2.8 Spin 代码来自清单 2.1,并做了一些小改动
Listing 2.8 Spin code from listing 2.1, with a couple of minor changes
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 FPSInput:MonoBehaviour {
公共浮动速度 = 6.0f; ❶
无效更新(){
变换.平移(0,速度,0); ❷
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FPSInput : MonoBehaviour {
public float speed = 6.0f; ❶
void Update() {
transform.Translate(0, speed, 0); ❷
}
}
❶ This will be too fast at first but will be corrected later.
❷ Change Rotate() to Translate().
这根据按键移动的代码与根据鼠标旋转的代码类似。GetAxis ()方法的使用方法也类似。此清单演示了如何使用它。
The code for moving according to keypresses is similar to the code for rotating according to the mouse. The GetAxis() method is used as well and in a similar way. This listing demonstrates how to use it.
Listing 2.9 Positional movement responding to keypresses
...
无效更新(){
float deltaX = Input.GetAxis("Horizontal") * speed; ❶
float deltaZ = Input.GetAxis("垂直") * 速度;
变换.平移(deltaX, 0,deltaZ);
}
......
void Update() {
float deltaX = Input.GetAxis("Horizontal") * speed; ❶
float deltaZ = Input.GetAxis("Vertical") * speed;
transform.Translate(deltaX, 0, deltaZ);
}
...
❶ Horizontal and Vertical are indirect names for keyboard mappings.
和以前一样,GetAxis()值乘以速度来确定移动量。以前,请求的轴始终是“鼠标某物”,现在我们传入的是Horizontal或Vertical。这些名称是 Unity 中输入设置的抽象;如果您查看 Project Settings 下的 Edit 菜单,然后查看 Input Manager 下的内容,您将找到一个抽象输入名称列表以及映射到这些名称的确切控件。左右箭头键和字母 A 和 D 都映射到Horizontal,而上下箭头键和字母 W 和 S 都映射到Vertical。
As before, the GetAxis() values are multiplied by speed to determine the amount of movement. Whereas before, the requested axis was always “Mouse something,” now we pass in either Horizontal or Vertical. These names are abstractions for input settings in Unity; if you look in the Edit menu under Project Settings and then look under Input Manager, you’ll find a list of abstract input names and the exact controls mapped to those names. Both the left and right arrow keys and the letters A and D are mapped to Horizontal, whereas both the up and down arrow keys and the letters W and S are mapped to Vertical.
请注意,移动值应用于 x 和 z 坐标。您在试验 Translate ()方法时可能注意到了这一点,x坐标左右移动,z坐标前后移动。
Note that the movement values are applied to the x- and z-coordinates. As you probably noticed while experimenting with the Translate() method, the x-coordinate moves from side to side, and the z-coordinate moves forward and backward.
输入这个新的移动代码,你应该能够通过按箭头键或 W、A、S 和 D 字母键来移动,这是大多数 FPS 游戏中的标准。移动脚本几乎完成了,但我们还需要进行一些调整超过。
Put in this new movement code and you should be able to move around by pressing either the arrow keys or W, A, S, and D letter keys, the standard in most FPS games. The movement script is nearly complete, but we have a few more adjustments to go over.
它是目前还不明显,因为你只在一台电脑(你的电脑)上运行代码,但如果你在不同的机器上运行代码,它会以不同的速度运行。这是因为有些电脑处理代码和图形的速度比其他电脑快。现在,玩家在不同的电脑上移动的速度会不同,因为移动代码与电脑的速度有关。这被称为帧速率依赖,因为移动代码依赖于游戏的帧速率。
It’s not obvious right now because you’ve been running the code on only one computer (yours), but if you ran the code on different machines, it’d run at different speeds. That’s because some computers can process code and graphics faster than others. Right now, the player would move at different speeds on different computers because the movement code is tied to the computer’s speed. That is referred to as frame-rate dependent, because the movement code is dependent on the frame rate of the game.
假设您在两台计算机上运行此演示,一台每秒获得 30 帧 (fps),另一台获得 60 fps。这意味着在第二台计算机上, Update() 的调用频率是原来的两倍,并且每次都会应用相同的速度值 6。在 30 fps 下,移动速率为 180 单位/秒,而在 60 fps 下,移动速率为 360 单位/秒。对于大多数游戏来说,像这样变化的移动速度将是一个坏消息。
Imagine you run this demo on two computers, one that gets 30 frames per second (fps) and one that gets 60 fps. That means Update() would be called twice as often on the second computer, and the same speed value of 6 would be applied every time. At 30 fps, the rate of movement would be 180 units/second, and the movement at 60 fps would be 360 units/second. For most games, movement speed that varies like this would be bad news.
解决方案是调整运动代码,使其与帧速率无关。此移动速度不依赖于游戏的帧速率。实现此目的的方法是不要在每个帧速率上应用相同的速度值。而是根据计算机的运行速度来调高或调低速度值。这是通过将速度值乘以另一个称为deltaTime的值来实现的。
The solution is to adjust the movement code to make it frame-rate independent. This speed of movement is not dependent on the frame rate of the game. The way to achieve this is by not applying the same speed value at every frame rate. Instead, scale the speed value higher or lower depending on how quickly the computer runs. This is achieved by multiplying the speed value by another value called deltaTime.
Listing 2.10 Frame-rate independent movement using deltaTime
...
无效更新(){
float deltaX = Input.GetAxis("水平") * 速度;
float deltaZ = Input.GetAxis("垂直") * 速度;
变换.Translate(deltaX * Time.deltaTime, 0, deltaZ * Time.deltaTime);
}
......
void Update() {
float deltaX = Input.GetAxis("Horizontal") * speed;
float deltaZ = Input.GetAxis("Vertical") * speed;
transform.Translate(deltaX * Time.deltaTime, 0, deltaZ * Time.deltaTime);
}
...
这是一个简单的改变。Time类具有对计时有用的属性和方法,其中一个属性是deltaTime。我们知道delta表示变化量,因此deltaTime是时间的变化量。具体来说,deltaTime是帧之间的时间量。帧之间的时间在不同的帧速率下会有所不同(例如,30 fps 的 deltaTime为1/30 秒),因此将速度值乘以deltaTime将在不同的计算机上缩放速度值。
That was a simple change. The Time class has properties and methods that are useful for timing, and one of those properties is deltaTime. We know that delta means the amount of change, so that means deltaTime is the amount of change in time. Specifically, deltaTime is the amount of time between frames. The time between frames varies at different frame rates (for example, 30 fps has a deltaTime of 1/30th of a second), so multiplying the speed value by deltaTime will scale the speed value on different computers.
现在,所有电脑上的移动速度都相同。但移动脚本还没有完全完成。当你在房间里移动时,你可以穿过墙壁,所以我们需要进一步调整代码以防止那。
Now the movement speed will be the same on all computers. But the movement script is still not quite done. When you move around the room, you can pass through walls, so we need to adjust the code further to prevent that.
直接地改变对象的变换不会应用碰撞检测,因此角色将穿过墙壁。要应用碰撞检测,我们想要做的是使用CharacterController,这是一个使对象更像游戏中的角色移动的组件,包括与墙壁碰撞。回想一下,当我们设置玩家时,我们附加了一个CharacterController ,所以现在我们将在FPSInput中将该组件与移动代码一起使用。
Directly changing the object’s transform doesn’t apply collision detection, so the character will pass through walls. To apply collision detection, what we want to do instead is use CharacterController, a component that makes the object move more like a character in a game, including colliding with walls. Recall that, back when we set up the player, we attached a CharacterController, so now we’ll use that component with the movement code in FPSInput.
清单 2.11 移动CharacterController而不是Transform
Listing 2.11 Moving CharacterController instead of Transform
... 私有CharacterController charController; ❶ 无效开始(){ charController = GetComponent<CharacterController>(); ❷ } 无效更新(){ float deltaX = Input.GetAxis("水平") * 速度; float deltaZ = Input.GetAxis("垂直") * 速度; Vector3 运动 = new Vector3(deltaX, 0, deltaZ); 运动 = Vector3.ClampMagnitude(运动,速度); ❸ 运动 *=时间.deltaTime; 运动 = transform.TransformDirection(运动); ❹ charController.Move(运动); ❺ } ...
... private CharacterController charController; ❶ void Start() { charController = GetComponent<CharacterController>(); ❷ } void Update() { float deltaX = Input.GetAxis("Horizontal") * speed; float deltaZ = Input.GetAxis("Vertical") * speed; Vector3 movement = new Vector3(deltaX, 0, deltaZ); movement = Vector3.ClampMagnitude(movement, speed); ❸ movement *= Time.deltaTime; movement = transform.TransformDirection(movement); ❹ charController.Move(movement); ❺ } ...
❶ Variable for referencing the CharacterController
❷ Access other components attached to the same object.
❸ Limit diagonal movement to the same speed as movement along an axis.
❹ Transform the movement vector from local to global coordinates.
❺告诉 CharacterController 按照该向量移动。
❺ Tell the CharacterController to move by that vector.
这段代码摘录介绍了几个新概念。首先要指出的概念是用于引用CharacterController 的变量。此变量创建对对象(即代码对象 — 不要与场景对象混淆)的本地引用;多个脚本可以引用这个CharacterController实例。
This code excerpt introduces several new concepts. The first concept to point out is the variable for referencing the CharacterController. This variable creates a local reference to the object (code object, that is—not to be confused with scene objects); multiple scripts can have references to this one CharacterController instance.
该变量最初为空,因此在使用引用之前,您需要为其分配一个对象以供引用。这就是GetComponent()发挥作用的地方;该方法返回附加到同一 GameObject 的其他组件。您无需在括号内传递参数,而是使用 C# 语法在尖括号<>内定义类型。
That variable starts out empty, so before you can use the reference, you need to assign an object for it to refer to. This is where GetComponent() comes into play; that method returns other components attached to the same GameObject. Rather than passing a parameter inside the parentheses, you use the C# syntax of defining the type inside angle brackets, <>.
获得对CharacterController 的引用后,即可在控制器上调用Move() 。将一个向量传递给该方法,类似于鼠标旋转代码使用向量来表示旋转值的方式。此外,与限制旋转值的方式类似,使用Vector3.ClampMagnitude()将向量的幅度限制为移动速度。之所以使用限幅,是因为否则对角线移动的幅度将大于直接沿轴移动的幅度(请看直角三角形的边和斜边)。
Once you have a reference to the CharacterController, you can call Move() on the controller. Pass in a vector to that method, similar to the way the mouse rotation code used a vector for rotation values. Also, similar to the way rotation values were limited, use Vector3.ClampMagnitude() to limit the vector’s magnitude to the movement speed. The clamp is used because, otherwise, diagonal movement would have a greater magnitude than movement directly along an axis (picture the sides and hypotenuse of a right triangle).
但是这里的移动向量有一个棘手的方面,它与局部与全局有关,正如我们之前讨论旋转时所讨论的那样。我们将创建一个向量,其值用于移动,例如向左移动。不过,这是玩家的左手,可能与世界的左手完全不同— 也就是说,我们谈论的是局部空间中的左手,而不是全局空间中的左手。
But there’s one tricky aspect to the movement vector here, and it has to do with local versus global, as we discussed earlier for rotations. We’ll create the vector with a value to move, say, to the left. That’s the player’s left, though, which may be a completely different direction from the world’s left—that is, we’re talking about left in local space, not global space.
我们需要将全局空间中定义的运动向量传递给Move()方法,因此我们需要将局部空间向量转换为全局空间向量。进行这种转换需要复杂的数学运算,但幸运的是,Unity 会为我们处理好这些数学运算,我们只需调用 TransformDirection ()方法即可为了改变方向。
We need to pass a movement vector defined in global space to the Move() method, so we’re going to need to convert the local space vector into a global space vector. Doing that conversion is complex math, but fortunately for us, Unity takes care of that math for us, and we simply need to call the TransformDirection() method in order to, well, transform the direction.
定义 变换在此上下文中,表示从一个坐标空间转换到另一个坐标空间(如果您不记得什么是坐标空间,请参阅第 2.3.3 节)。不要与变换的其他定义混淆,包括变换组件和在场景中移动对象的动作。它是一种重载术语,因为所有这些含义都指的是相同的底层概念。
DEFINITION Transform in this context means to convert from one coordinate space to another (refer to section 2.3.3 if you don’t remember what a coordinate space is). Don’t get confused with the other definitions of transform, including both the Transform component and the action of moving the object around the scene. It’s sort of an overloaded term, because all these meanings refer to the same underlying concept.
现在测试播放移动代码。如果您还没有这样做,请将MouseLook组件设置为水平和垂直旋转。您可以完全环顾场景并使用键盘控制在场景中飞行。如果您希望玩家在场景中飞行,这非常棒,但是如果您希望玩家走路而不是飞行?
Test playing the movement code now. If you haven’t done so already, set the MouseLook component to both horizontal and vertical rotation. You can look around the scene fully and fly around the scene by using keyboard controls. This is pretty great if you want the player to fly around the scene, but what if you want the player walking instead of flying?
现在碰撞检测工作正常,脚本可以有重力,玩家将靠在地板上。声明一个重力变量并将该值用作 y 轴。
Now that collision detection is working, the script can have gravity, and the player will stay down against the floor. Declare a gravity variable and use that value for the y-axis.
Listing 2.12 Adding gravity to the movement code
...
公共浮动重力= -9.8f;
...
无效更新(){
...
运动 = Vector3.ClampMagnitude(运动,速度);
movement.y = 重力; ❶
运动 *=时间.deltaTime;
......
public float gravity = -9.8f;
...
void Update() {
...
movement = Vector3.ClampMagnitude(movement, speed);
movement.y = gravity; ❶
movement *= Time.deltaTime;
...
❶ Use the gravity value instead of just 0.
现在玩家身上有一个恒定的向下力,但它并不总是垂直向下,因为玩家对象可以随着鼠标上下倾斜。幸运的是,我们需要修复的所有问题都已就绪,因此我们只需要对玩家上组件的设置方式进行微小调整。首先,将玩家对象上的MouseLook组件设置为仅水平旋转。将MouseLook组件添加到相机对象,并将其设置为仅垂直旋转。没错;您将有两个对象响应鼠标!
Now there’s a constant downward force on the player, but it’s not always pointed straight down, because the player object can tilt up and down with the mouse. Fortunately, everything we need to fix that is already in place, so we need only to make minor adjustments to the way components are set up on the player. First, set the MouseLook component on the player object to horizontal rotation only. Add the MouseLook component to the camera object, and set that one to vertical rotation only. That’s right; you’re going to have two objects responding to the mouse!
因为玩家对象现在只能水平旋转,所以向下倾斜的重力不再存在任何问题。相机对象是玩家对象的父级(还记得我们在层次结构视图中这样做吗?),因此即使相机独立于玩家垂直旋转,相机也会随玩家一起水平旋转。
Because the player object now only rotates horizontally, there’s no longer any problem with the downward force of gravity being tilted. The camera object is parented to the player object (remember when we did that in the Hierarchy view?), so even though the camera rotates vertically independently from the player, the camera rotates horizontally along with the player.
清单 2.13 显示了完整的完成脚本。随着对玩家身上组件设置方式的细微调整,玩家可以在房间内走动。即使应用了重力变量,您仍然可以通过在 Inspector 中将重力设置为 0 来使用此脚本进行飞行运动。
Listing 2.13 shows the full finished script. Along with the small adjustments to the way components are set up on the player, the player can walk around the room. Even with the gravity variable being applied, you can still use this script for flying movement by setting Gravity to 0 in the Inspector.
Listing 2.13 The finished FPSInput script
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
[RequireComponent(typeof(CharacterController))]
[AddComponentMenu("控制脚本/FPS 输入")]
公共类 FPSInput:MonoBehaviour {
公共浮动速度=6.0f;
公共浮动重力= -9.8f;
私人CharacterController charController;
无效开始(){
charController = GetComponent<CharacterController>();
}
无效更新(){
float deltaX = Input.GetAxis("水平") * 速度;
float deltaZ = Input.GetAxis("垂直") * 速度;
Vector3 运动 = new Vector3(deltaX, 0, deltaZ);
运动 = Vector3.ClampMagnitude(运动,速度);
运动.y = 重力;
运动 *=时间.deltaTime;
运动 = 变换.TransformDirection(运动);
charController.移动(运动);
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
[AddComponentMenu("Control Script/FPS Input")]
public class FPSInput : MonoBehaviour {
public float speed = 6.0f;
public float gravity = -9.8f;
private CharacterController charController;
void Start() {
charController = GetComponent<CharacterController>();
}
void Update() {
float deltaX = Input.GetAxis("Horizontal") * speed;
float deltaZ = Input.GetAxis("Vertical") * speed;
Vector3 movement = new Vector3(deltaX, 0, deltaZ);
movement = Vector3.ClampMagnitude(movement, speed);
movement.y = gravity;
movement *= Time.deltaTime;
movement = transform.TransformDirection(movement);
charController.Move(movement);
}
}
恭喜你完成了这个 3D 项目!我们在本章中讲解了很多内容,现在你已经熟练掌握了如何在 Unity 中编写运动代码。虽然这个第一个演示很令人兴奋,但距离成为一个完整的游戏还有很长的路要走。毕竟,项目计划将其描述为一个基本的 FPS 场景,如果你不能射击,射击游戏又有什么意义呢?所以,为本章的项目给自己一个当之无愧的赞许,然后为下一章做好准备。下一个步。
Congratulations on building this 3D project! We covered a lot of ground in this chapter, and now you’re well versed in how to code movement in Unity. As exciting as this first demo is, it’s still a long way from being a complete game. After all, the project plan described this as a basic FPS scene, and what’s a shooter if you can’t shoot? So give yourself a well-deserved pat on the back for this chapter’s project and then get ready for the next step.
上一章中的移动演示非常酷,但仍然算不上真正的游戏。让我们把那个移动演示变成第一人称射击游戏。如果你考虑一下我们现在还需要什么,那归根结底就是射击能力和可以射击的东西。
The movement demo from the previous chapter was pretty cool but still not really a game. Let’s turn that movement demo into a first-person shooter. If you think about what else we need now, it boils down to the ability to shoot and having things to shoot at.
首先,我们将编写脚本,使玩家能够射击场景中的物体。然后,我们将构建敌人来填充场景,包括漫无目的地游荡和对被击中做出反应的代码。最后,我们将使敌人能够反击,向玩家发射火球。第 2 章中的任何脚本都不需要更改;相反,我们将向项目添加脚本 - 处理附加功能的脚本。
First, we’re going to write scripts that enable the player to shoot objects in the scene. Then, we’re going to build enemies to populate the scene, including code to both wander around aimlessly and react to being hit. Finally, we’re going to enable the enemies to fight back, emitting fireballs at the player. None of the scripts from chapter 2 need to change; instead, we’ll add scripts to the project—scripts that handle the additional features.
我为这个项目选择了第一人称射击游戏,原因有几个。其中一个原因很简单,因为 FPS 游戏很受欢迎:人们喜欢射击游戏,所以让我们制作一款射击游戏吧。一个更微妙的原因与你将要学习的技术有关;这个项目是学习 3D 模拟中几个基本概念的好方法。例如,射击游戏是教授光线投射的好方法。稍后,我们将详细介绍它是什么,但现在,你只需要知道它是 3D 模拟中许多任务的一个有用概念。虽然光线投射在各种情况下都很有用,但使用光线投射对射击来说最直观。
I’ve chosen a first-person shooter for this project for a couple of reasons. One is simply that FPS games are popular: people like shooting games, so let’s make a shooting game. A subtler reason has to do with the techniques you’ll learn; this project is a great way to learn about several fundamental concepts in 3D simulations. For example, shooting games are a great way to teach raycasting. In a bit, we’ll get into the specifics of what that is, but for now, you need to know only that it’s a useful concept for many tasks in 3D simulations. Although raycasting is useful in a wide variety of situations, it just so happens that using raycasting makes the most intuitive sense for shooting.
创建游荡目标进行射击为我们提供了一个很好的借口来探索计算机控制角色的代码,以及使用发送消息和生成对象的技术。事实上,这种游荡行为是光线投射的另一个有价值的地方,因此在通过射击首次了解该技术后,我们已经开始研究该技术的不同应用。同样,本项目中演示的发送消息的方法在其他地方也很有用。在以后的章节中,您将看到这些技术的其他应用,即使在这个项目中,我们也会讨论其他情况。
Creating wandering targets to shoot at gives us a great excuse to explore code for computer-controlled characters, as well as to use techniques for sending messages and spawning objects. In fact, this wandering behavior is another place that raycasting is valuable, so we’re already going to be looking at a different application of the technique after having first learned about it with shooting. Similarly, the approach to sending messages that’s demonstrated in this project is also useful elsewhere. In future chapters, you’ll see other applications for these techniques, and even within this one project we’ll go over alternative situations.
最终,我们将一步一步地完成这个项目,让游戏在每一步都可玩,但同时也总觉得还有一部分需要继续努力。此路线图将步骤分解为小的、易于理解的更改,每次只添加一项新功能:
Ultimately, we’ll approach this project one new feature at a time, with the game always playable at every step, but also always feeling like there’s a missing part to work on next. This road map breaks the steps into small, understandable changes, with only one new feature added at a time:
注意本章的项目假设您已经有一个第一人称移动演示可供使用。我们在第 2 章中创建了一个移动演示,但如果您直接跳到本章,则需要下载第 2 章的示例文件。
NOTE This chapter’s project assumes you already have a first-person movement demo to build on. We created a movement demo in chapter 2, but if you skipped straight to this chapter, you will need to download the sample files for chapter 2.
这3D 演示中引入的第一个新功能是射击。环顾四周和移动对于第一人称射击游戏来说无疑是至关重要的功能,但只有当玩家能够影响模拟并运用他们的技能时,它才算得上是游戏。3D 游戏中的射击可以通过几种方法实现,其中最重要的方法之一就是光线投射。
The first new feature to introduce into the 3D demo is shooting. Looking around and moving are certainly crucial features for a first-person shooter, but it’s not a game until players can affect the simulation and apply their skills. Shooting in 3D games can be implemented with a few approaches, and one of the most important approaches is raycasting.
作为顾名思义,射线投射会将射线投射到场景中。很清楚,对吧?好吧,那么射线到底是什么?
As the name indicates, raycasting casts a ray into the scene. Clear, right? Well, okay, so what exactly is a ray?
定义射线是场景中的一条假想或不可见的线,从原点开始并向特定方向延伸。
DEFINITION A ray is an imaginary or invisible line in the scene that starts at a point of origin and extends out in a specific direction.
在射线投射中,您创建一条射线,然后确定与它相交的物体。图 3.1 说明了这一概念。想象一下当您用枪发射子弹时会发生什么:子弹从枪的位置开始,然后沿直线向前飞行,直到击中某物。射线类似于子弹的路径,射线投射类似于发射子弹并观察它击中了什么。
In raycasting, you create a ray and then determine what intersects it. Figure 3.1 illustrates the concept. Consider what happens when you fire a bullet from a gun: the bullet starts at the position of the gun and then flies forward in a straight line until it hits something. A ray is analogous to the path of the bullet, and raycasting is analogous to firing the bullet and seeing what it hits.
图 3.1 射线是一条假想的线,射线投射就是寻找该线的相交点。
Figure 3.1 A ray is an imaginary line, and raycasting is finding where that line intersects.
可以想象,光线投射背后的数学运算通常很复杂。不仅计算一条线与 3D 平面的交点很棘手,而且您需要对场景中所有网格对象的所有多边形进行此操作(请记住,网格对象是由许多连接的线和形状构成的 3D 视觉对象)。幸运的是,Unity 处理了光线投射背后的复杂数学运算,但您仍然需要担心更高层次的问题,例如光线从哪里投射以及为什么投射。
As you can imagine, the math behind raycasting often gets complicated. Not only is it tricky to calculate the intersection of a line with a 3D plane, but you need to do that for all polygons of all mesh objects in the scene (remember, a mesh object is a 3D visual constructed from lots of connected lines and shapes). Fortunately, Unity handles the difficult math behind raycasting, but you still have to worry about higher-level concerns such as where the ray is being cast from and why.
在这个项目中,后一个问题(为什么)的答案是模拟一颗子弹射入场景。对于第一人称射击游戏,射线通常从相机位置开始,然后延伸到相机视图的中心。换句话说,您正在检查相机正前方的物体;Unity 提供了命令来简化这项任务。让我们看看这些命令。
In this project, the answer to the latter question (why) is to simulate a bullet being fired into the scene. For a first-person shooter, the ray generally starts at the camera position and then extends out through the center of the camera view. In other words, you’re checking for objects straight in front of the camera; Unity provides commands to make that task simple. Let’s look at these commands.
你会通过投射从相机开始并穿过视图中心向前延伸的射线来实现射击。Unity 提供了ScreenPointToRay()方法执行此操作。
You’ll implement shooting by projecting a ray that starts at the camera and extends forward through the center of the view. Unity provides the ScreenPointToRay() method to perform this action.
图 3.2 说明了调用此方法时发生的情况。它创建一条从相机开始并以一定角度投射的射线,穿过给定的屏幕坐标。通常,鼠标位置的坐标用于鼠标拾取(选择鼠标下方的物体),但对于第一人称射击,则使用屏幕中心。一旦有了射线,就可以将其传递给 Physics.Raycast ()方法使用该射线执行射线投射。
Figure 3.2 illustrates what happens when this method is invoked. It creates a ray that starts at the camera and projects at an angle, passing through the given screen coordinates. Usually, the coordinates of the mouse position are used for mouse picking (selecting the object under the mouse), but for first-person shooting, the center of the screen is used. Once you have a ray, it can be passed to the Physics.Raycast() method to perform raycasting using that ray.
图 3.2 ScreenPointToRay()通过给定的屏幕坐标投射一条来自相机的射线。
Figure 3.2 ScreenPointToRay() projects a ray from the camera through the given screen coordinates.
让我们编写使用我们刚刚讨论的方法的代码。在 Unity 中,创建一个新的 C# 脚本,将其命名为RayShooter,将该脚本附加到相机(而不是玩家对象),然后在其中写入此列表中的代码。
Let’s write code that uses the methods we just discussed. In Unity, create a new C# script, call it RayShooter, attach that script to the camera (not the player object), and then write the code from this listing in it.
Listing 3.1 RayShooter script to attach to the camera
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 RayShooter:MonoBehaviour {
私人摄像机摄像头;
无效开始(){
cam = GetComponent<Camera>(); ❶
}
无效更新(){
if (Input.GetMouseButtonDown(0)) { ❷
Vector3 point = new Vector3(cam.pixelWidth/2, cam.pixelHeight/2, 0); ❸
Ray ray = cam.ScreenPointToRay(point); ❹
RaycastHit 命中;
if (Physics.Raycast(ray, out hit)) { ❺
Debug.Log("Hit " + hit.point); ❻
}
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RayShooter : MonoBehaviour {
private Camera cam;
void Start() {
cam = GetComponent<Camera>(); ❶
}
void Update() {
if (Input.GetMouseButtonDown(0)) { ❷
Vector3 point = new Vector3(cam.pixelWidth/2, cam.pixelHeight/2, 0); ❸
Ray ray = cam.ScreenPointToRay(point); ❹
RaycastHit hit;
if (Physics.Raycast(ray, out hit)) { ❺
Debug.Log("Hit " + hit.point); ❻
}
}
}
}
❶ Access other components attached to the same object.
❷ Respond to the left (first) mouse button.
❸ The middle of the screen is half its width and height.
❹使用ScreenPointToRay()在该位置创建射线。
❹ Create the ray at that position by using ScreenPointToRay().
❺ The raycast fills a referenced variable with information.
❻ Retrieve coordinates where the ray hit.
您应该注意此代码清单中的几点。首先,Camera组件在Start()中检索,就像上一章中的CharacterController一样。然后,其余代码放在Update()中,因为它需要反复检查鼠标,而不是只检查一次。Input.GetMouseButtonDown ()方法返回true或false,具体取决于鼠标是否被单击,因此将该命令放在条件中意味着封闭的代码仅在鼠标被单击时运行。您希望在玩家单击鼠标时进行射击 — 因此需要对鼠标按钮进行条件检查。
You should note several things in this code listing. First, the Camera component is retrieved in Start(), just like the CharacterController in the previous chapter. Then, the rest of the code is put in Update() because it needs to check the mouse repeatedly, as opposed to just one time. The Input.GetMouseButtonDown() method returns true or false, depending on whether the mouse has been clicked, so putting that command in a conditional means the enclosed code runs only when the mouse has been clicked. You want to shoot when the player clicks the mouse—hence the conditional check of the mouse button.
创建一个向量来定义射线的屏幕坐标(请记住,向量是存储在一起的几个相关数字)。相机的pixelWidth和pixelHeight值给出了屏幕的大小,因此将这些值除以二即可得到屏幕的中心。虽然屏幕坐标是 2D 的,只有水平和垂直分量,没有深度,但还是创建了一个Vector3 ,因为ScreenPointToRay()需要该数据类型(大概是因为计算射线涉及 3D 向量的算术)。使用这组坐标调用ScreenPointToRay(),得到一个Ray对象(代码对象,而不是游戏对象;两者有时会混淆)。
A vector is created to define the screen coordinates for the ray (remember that a vector is several related numbers stored together). The camera’s pixelWidth and pixelHeight values give you the size of the screen, so dividing those values in half gives you the center of the screen. Although screen coordinates are 2D, with only horizontal and vertical components and no depth, a Vector3 was created because ScreenPointToRay() requires that data type (presumably because calculating the ray involves arithmetic on 3D vectors). ScreenPointToRay() was called with this set of coordinates, resulting in a Ray object (a code object, not a game object; the two can be confused sometimes).
然后将射线传递给Raycast()方法,但它并不是传入的唯一对象。还有一个RaycastHit数据结构;RaycastHit是关于射线相交的一组信息,包括相交发生的位置以及相交的对象。C# 语法out确保命令中操作的数据结构与命令外部存在的对象相同,而不是对象是不同函数范围内的单独副本。
The ray is then passed to the Raycast() method, but it’s not the only object passed in. There’s also a RaycastHit data structure; RaycastHit is a bundle of information about the intersection of the ray, including where the intersection happened and what object was intersected. The C# syntax out ensures that the data structure manipulated within the command is the same object that exists outside the command, as opposed to the objects being separate copies in the different function scopes.
有了这些参数,Physics.Raycast()方法就可以工作了。此方法检查与给定射线的交点,填写有关交点的数据,如果射线碰到任何东西,则返回true。由于返回的是布尔值,因此可以将此方法置于条件检查中,就像您之前使用Input.GetMouseButtonDown()一样。
With those parameters in place, the Physics.Raycast() method can do its work. This method checks for intersections with the given ray, fills in data about the intersection, and returns true if the ray hit anything. Because a Boolean value is returned, this method can be put in a conditional check, just as you used Input.GetMouseButtonDown() earlier.
目前,代码发出一条控制台消息来指示何时发生交点。此控制台消息显示射线击中点的 3D 坐标(我们在第 2 章中讨论的 x、y、z 值)。但很难想象射线击中的具体位置;同样,很难分辨屏幕中心的位置(射线穿过的位置)。让我们添加视觉指示器来解决这两个问题问题。
For now, the code emits a console message to indicate when an intersection occurred. This console message displays the 3D coordinates of the point where the ray hit (the x, y, z values we discussed in chapter 2). But it can be hard to visualize where exactly the ray hit; similarly, it can be hard to tell where the center of the screen is (the location where the ray shoots through). Let’s add visual indicators to address both problems.
我们的下一步是添加两种视觉指示器:屏幕中心的瞄准点和场景中射线击中的标记。对于第一人称射击游戏,后者通常是弹孔,但现在,您将在该点上放置一个空白球体(并使用协同程序在 1 秒后移除球体)。图 3.3 显示了您将看到的内容。
Our next step is to add two kinds of visual indicators: an aiming spot at the center of the screen and a mark in the scene where the ray hit. For a first-person shooter, the latter is usually bullet holes, but for now, you’re going to put a blank sphere on the spot (and use a coroutine to remove the sphere after 1 second). Figure 3.3 shows what you’ll see.
定义 协程是一种处理随时间逐步执行的任务的方法。相比之下,大多数函数会让程序等待直到它们完成。
DEFINITION Coroutines are a way of handling tasks that execute incrementally over time. In contrast, most functions make the program wait until they finish.
首先,让我们添加指示器来标记射线击中的位置。清单 3.2 显示了添加后的代码。在场景中奔跑、射击;看到球体指示器真是太有趣了!
First, let’s add indicators to mark where the ray hits. Listing 3.2 shows the script after making this addition. Run around the scene, shooting; it’s pretty fun seeing the sphere indicators!
Figure 3.3 Shooting repeatedly after adding visual indicators for aiming and hits
Listing 3.2 RayShooter script with sphere indicators added
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 RayShooter:MonoBehaviour {
私人摄像机摄像头;
无效开始(){
cam = GetComponent<相机>();
}
void Update() { ❶
如果(输入.GetMouseButtonDown(0)){
Vector3 点 = 新 Vector3(cam.pixelWidth/2, cam.pixelHeight/2, 0);
射线 ray = cam.ScreenPointToRay(point);
RaycastHit 命中;
如果 (Physics.Raycast(ray, out hit)) {
启动协同程序(SphereIndicator(hit.point)); ❷
}
}
}
私有 IEnumerator SphereIndicator(Vector3 pos) { ❸
游戏对象球体 = 游戏对象.创建原始 (原始类型.球体);
球体.变换.位置 = pos;
产生返回新的WaitForSeconds(1); ❹
摧毁(球体); ❺
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RayShooter : MonoBehaviour {
private Camera cam;
void Start() {
cam = GetComponent<Camera>();
}
void Update() { ❶
if (Input.GetMouseButtonDown(0)) {
Vector3 point = new Vector3(cam.pixelWidth/2, cam.pixelHeight/2, 0);
Ray ray = cam.ScreenPointToRay(point);
RaycastHit hit;
if (Physics.Raycast(ray, out hit)) {
StartCoroutine(SphereIndicator(hit.point)); ❷
}
}
}
private IEnumerator SphereIndicator(Vector3 pos) { ❸
GameObject sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
sphere.transform.position = pos;
yield return new WaitForSeconds(1); ❹
Destroy(sphere); ❺
}
}
❶ This function is mostly the same raycasting code from listing 3.1.
❷ Launch a coroutine in response to a hit.
❸ Coroutines use IEnumerator functions.
❹ The yield keyword tells coroutines where to pause.
❺ Remove this GameObject and clear its memory.
新的方法是SphereIndicator() ,并在现有的Update()方法中添加一行修改。此方法在场景中的某个点创建一个球体,然后在一秒钟后移除该球体。从射线投射代码中调用SphereIndicator()可确保有视觉指示器准确显示射线击中的位置。此函数使用IEnumerator定义,该类型与协程的概念相关。
The new method is SphereIndicator(), plus a one-line modification in the existing Update() method. This method creates a sphere at a point in the scene and then removes that sphere a second later. Calling SphereIndicator() from the raycasting code ensures that there will be visual indicators showing exactly where the ray hit. This function is defined with IEnumerator, and that type is tied in with the concept of coroutines.
从技术上讲,协程不是异步的(异步操作不会阻止其余代码运行;想想在网站脚本中下载图像),但通过巧妙使用枚举器,Unity 使协程的行为类似于异步函数。协程中的秘诀是yield关键字; 该关键字使协程暂时暂停,交还程序流程并在下一帧中再次从该点继续执行。这样,协程似乎在程序的后台运行,通过重复的循环运行部分程序,然后返回到程序的其余部分。
Technically, coroutines aren’t asynchronous (asynchronous operations don’t stop the rest of the code from running; think of downloading an image in the script of a website), but through clever use of enumerators, Unity makes coroutines behave similarly to asynchronous functions. The secret sauce in coroutines is the yield keyword; that keyword causes the coroutine to temporarily pause, handing back the program flow and picking up again from that point in the next frame. In this way, coroutines seemingly run in the background of a program, through a repeated cycle of running partway and then returning to the rest of the program.
顾名思义,StartCoroutine()启动一个协程。一旦启动协程,它就会一直运行,直到函数完成;它会在过程中暂停。请注意传递给StartCoroutine() 的方法在名称后面有一组括号,这个微妙但重要的一点是:此语法表示您正在调用该函数,而不是传递其名称。被调用的函数会一直运行,直到遇到一个yield命令,此时函数暂停。
As the name indicates, StartCoroutine() sets a coroutine in motion. Once a coroutine is started, it keeps running until the function is finished; it pauses along the way. Note the subtle but significant point that the method passed to StartCoroutine() has a set of parentheses following the name: this syntax means you’re calling that function, as opposed to passing its name. The called function runs until it hits a yield command, at which point the function pauses.
SphereIndicator()在特定点创建一个球体,暂停执行yield语句,然后在协程恢复后销毁该球体。暂停的长度由yield返回的值控制。在协程中可以使用几种类型的返回值,但最直接的是返回特定的等待时间长度。返回WaitForSeconds(1)会导致协程暂停 1 秒。创建一个球体,暂停 1 秒,然后销毁该球体:该序列设置了一个临时的视觉指示器。
SphereIndicator() creates a sphere at a specific point, pauses for the yield statement, and then destroys the sphere after the coroutine resumes. The length of the pause is controlled by the value returned at yield. A few types of return values work in coroutines, but the most straightforward is to return a specific length of time to wait. Returning WaitForSeconds(1) causes the coroutine to pause for 1 second. Create a sphere, pause for 1 second, and then destroy the sphere: that sequence sets up a temporary visual indicator.
清单 3.2 给出了指示符来标记射线击中的位置。但您还需要在屏幕中心有一个瞄准点。
Listing 3.2 gave you indicators to mark where the ray hits. But you also want an aiming spot in the center of the screen.
Listing 3.3 Visual indicator for aiming
...
无效开始(){
cam = GetComponent<相机>();
Cursor.lockState = CursorLockMode.Locked; ❶
Cursor.visible = false; ❶
}
无效的OnGUI(){
int 大小 = 12; ❷
浮点数posX = cam.pixelWidth/2-size/4;
浮点数 posY = cam.pixelHeight/2 - size/2;
GUI.Label(new Rect(posX, posY, size, size),“*”); ❸
}
......
void Start() {
cam = GetComponent<Camera>();
Cursor.lockState = CursorLockMode.Locked; ❶
Cursor.visible = false; ❶
}
void OnGUI() {
int size = 12; ❷
float posX = cam.pixelWidth/2 - size/4;
float posY = cam.pixelHeight/2 - size/2;
GUI.Label(new Rect(posX, posY, size, size), "*"); ❸
}
...
❶ Hide the mouse cursor at the center of the screen.
❷ This is just the rough size of this font.
❸ The GUI.Label() command displays text onscreen.
RayShooter类中添加了另一个新方法,称为OnGUI()。 Unity 附带基本和更高级的 UI 系统。 由于基本系统有很多限制,我们将在后续章节中构建更灵活的高级 UI,但目前,使用基本 UI 在屏幕中心显示一个点要容易得多。 与Start()和Update()非常相似,每个MonoBehaviour都会自动响应 OnGUI ()方法该函数在 3D 场景渲染之后立即运行每一帧,从而使OnGUI()期间绘制的所有内容都出现在 3D 场景的顶部(想象一下贴在风景画上的贴纸)。
Another new method has been added to the RayShooter class, called OnGUI(). Unity comes with both a basic and more advanced UI system. Because the basic system has a lot of limitations, we’ll build a more flexible advanced UI in future chapters, but for now, it’s much easier to display a point in the center of the screen by using the basic UI. Much like Start() and Update(), every MonoBehaviour automatically responds to an OnGUI() method. That function runs every frame right after the 3D scene is rendered, resulting in everything drawn during OnGUI() appearing on top of the 3D scene (imagine stickers applied to a painting of a landscape).
定义 渲染是计算机绘制 3D 场景像素的操作。尽管场景是使用 x、y 和 z 坐标定义的,但显示器上的实际显示是彩色像素的 2D 网格。要显示 3D 场景,计算机需要计算 2D 网格中所有像素的颜色;运行该算法称为渲染。
DEFINITION Render is the action of the computer drawing the pixels of the 3D scene. Although the scene is defined using x-, y-, and z-coordinates, the actual display on your monitor is a 2D grid of colored pixels. To display the 3D scene, the computer needs to calculate the color of all the pixels in the 2D grid; running that algorithm is referred to as rendering.
在OnGUI()中,代码定义显示的 2D 坐标(略微偏移以适应标签的大小),然后调用GUI.Label()。该方法显示一个文本标签。由于传递给标签的字符串是星号 ( * ),因此最终会将该字符显示在屏幕中央。现在,在我们刚刚起步的 FPS 游戏中瞄准变得容易多了!
Inside OnGUI(), the code defines 2D coordinates for the display (shifted slightly to account for the size of the label) and then calls GUI.Label(). That method displays a text label. Because the string passed to the label is an asterisk (*), you end up with that character displayed in the center of the screen. Now it’s much easier to aim in our nascent FPS game!
清单 3.3 还向Start()方法添加了光标设置。所发生的一切就是设置光标可见性和锁定的值。如果省略光标值,脚本将完美运行,但这些设置使第一人称控制工作得更顺畅一些。鼠标光标将停留在屏幕中央,为了避免使视图混乱,它将变为不可见,并且只有当您按 Esc 时才会重新出现。
Listing 3.3 also adds cursor settings to the Start() method. All that’s happening is that the values are being set for cursor visibility and locking. The script will work perfectly fine if you omit the cursor values, but these settings make first-person controls work a bit more smoothly. The mouse cursor will stay in the center of the screen, and to avoid cluttering the view, will turn invisible and will reappear only when you press Esc.
警告请务必记住,您可以按 Esc 键解锁鼠标光标,以便将其移出游戏视图的中间。当鼠标光标被锁定时,无法单击“播放”按钮并停止游戏。
WARNING Always remember that you can press Esc to unlock the mouse cursor in order to move it away from the middle of the Game view. While the mouse cursor is locked, it’s impossible to click the Play button and stop the game.
这结束了第一人称射击代码……好吧,无论如何,这结束了玩家的互动,但我们仍然需要注意的目 标。
That wraps up the first-person shooting code . . . well, that wraps up the player’s end of the interaction, anyway, but we still need to take care of targets.
存在能够射击固然很好,但目前玩家还没有任何可以射击的东西。我们将创建一个目标对象,并为其提供一个脚本,使其在被击中时做出响应。或者说,我们将稍微修改射击代码,使其在被击中时通知目标,然后目标上的脚本将在收到通知时做出反应。
Being able to shoot is all well and good, but at the moment, players don’t have anything to shoot at. We’re going to create a target object and give it a script that will respond to being hit. Or rather, we’ll slightly modify the shooting code to notify the target when hit, and then the script on the target will react when notified.
第一的,您需要创建一个新的对象来射击。创建一个新的立方体对象(GameObject > 3D Object > Cube),然后通过将 Y 比例设置为 2 并将 X 和 Z 保留为 1 来垂直放大它。将新对象放置在 0、1、0处,将其放在房间中间的地板上,并将该对象命名为Enemy。
First, you need to create a new object to shoot at. Create a new cube object (GameObject > 3D Object > Cube) and then scale it up vertically by setting the Y scale to 2 and leaving X and Z at 1. Position the new object at 0, 1, 0 to put it on the floor in the middle of the room, and name the object Enemy.
创建一个名为ReactiveTarget 的新脚本并将其附加到新创建的框。很快,您将为该脚本编写代码,但目前将其保留为默认设置;您提前创建此脚本文件是因为下一个代码清单需要它存在才能进行编译。
Create a new script called ReactiveTarget and attach that to the newly created box. Soon, you’ll write code for this script, but leave it as the default for now; you’re creating this script file ahead of time because the next code listing requires it to exist in order to compile.
返回RayShooter并根据以下清单修改光线投射代码。运行新代码并射击新目标;调试消息出现在控制台中,而不是场景中的球体指示器。
Go back to RayShooter and modify the raycasting code according to the following listing. Run the new code and shoot the new target; debug messages appear in the console instead of sphere indicators in the scene.
Listing 3.4 Detecting whether the target object was hit
...
如果 (Physics.Raycast(ray, out hit)) {
GameObject hitObject = hit.transform.gameObject; ❶
ReactiveTarget 目标 = hitObject.GetComponent<ReactiveTarget>();
如果 (target != null) { ❷
Debug.Log("目标命中");
} 别的 {
启动协同程序(SphereIndicator(hit.point));
}
}
......
if (Physics.Raycast(ray, out hit)) {
GameObject hitObject = hit.transform.gameObject; ❶
ReactiveTarget target = hitObject.GetComponent<ReactiveTarget>();
if (target != null) { ❷
Debug.Log("Target hit");
} else {
StartCoroutine(SphereIndicator(hit.point));
}
}
...
❶ Retrieve the object the ray hit.
❷ Check for the ReactiveTarget component on the object.
请注意,您从RaycastHit检索对象,就像检索球体指示器的坐标一样。从技术上讲,命中信息不会返回游戏对象命中;它指示Transform组件命中。然后,你可以将gameObject作为transform的属性来访问。
Notice that you retrieve the object from RaycastHit, just as the coordinates were retrieved for the sphere indicators. Technically, the hit information doesn’t return the game object hit; it indicates the Transform component hit. You can then access gameObject as a property of transform.
然后,使用GetComponent()方法在对象上检查它是否是反应目标(即是否附加了ReactiveTarget脚本)。如前所述,该方法返回附加到 GameObject 的特定类型的组件。如果没有该类型的组件附加到对象,则GetComponent()将不会返回任何内容。检查是否返回了null ,并在每种情况下运行不同的代码。
Then, you use the GetComponent()method on the object to check whether it’s a reactive target (that is, whether it has the ReactiveTarget script attached). As you saw previously, that method returns components of a specific type that are attached to the GameObject. If no component of that type is attached to the object, GetComponent() won’t return anything. You check whether null was returned and run different code in each case.
如果命中对象是反应性目标,代码将发出调试消息,而不是启动球体指示器的协程。现在让我们将命中情况通知目标对象,以便它可以反应。
If the hit object is a reactive target, the code emits a debug message instead of starting the coroutine for sphere indicators. Now let’s inform the target object about the hit so it can react.
All that’s needed in the code is a one-line change, as shown next.
Listing 3.5 Sending a message to the target object
...
如果 (目标 != 空) {
目标.ReactToHit(); ❶
} 别的 {
启动协同程序(SphereIndicator(hit.point));
}
......
if (target != null) {
target.ReactToHit(); ❶
} else {
StartCoroutine(SphereIndicator(hit.point));
}
...
❶ Call a method of the target instead of just emitting the debug message.
现在射击代码调用目标的一个方法,所以让我们编写那个目标方法。在ReactiveTarget脚本中,写入下一个清单中的代码。射击目标物体时,目标物体会倒下并消失;参考图 3.4。
Now the shooting code calls a method of the target, so let’s write that target method. In the ReactiveTarget script, write in the code from the next listing. The target object will fall over and disappear when you shoot it; refer to figure 3.4.
Listing 3.6 ReactiveTarget script that dies when hit
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 ReactiveTarget : MonoBehaviour {
public void ReactToHit() { ❶
启动协同程序(Die());
}
私有 IEnumerator Die() { ❷
这个.变换.旋转(-75,0,0);
产生返回新的WaitForSeconds(1.5f);
销毁(this.gameObject); ❸
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ReactiveTarget : MonoBehaviour {
public void ReactToHit() { ❶
StartCoroutine(Die());
}
private IEnumerator Die() { ❷
this.transform.Rotate(-75, 0, 0);
yield return new WaitForSeconds(1.5f);
Destroy(this.gameObject); ❸
}
}
❶ Method called by the shooting script
❷ Topple the enemy, wait 1.5 seconds, and then destroy the enemy.
❸ A script can destroy itself (just as it could a separate object).
您应该熟悉前面的脚本中的大部分代码,因此我们只简要介绍一下。首先,定义ReactToHit()方法,因为这是射击脚本中调用的方法名称。此方法启动一个协程,类似于之前的球体指示器代码;主要区别在于它操作此脚本的对象,而不是创建单独的对象。像this.gameObject这样的表达式指的是此脚本附加到的 GameObject(并且this关键字是可选的,因此代码可以引用gameObject,而前面没有任何内容)。
Most of this code should be familiar to you from previous scripts, so we’ll go over it only briefly. First, you define the ReactToHit()method, because that’s the method name called in the shooting script. This method starts a coroutine that’s similar to the sphere indicator code from earlier; the main difference is that it operates on the object of this script rather than creating a separate object. Expressions like this.gameObject refer to the GameObject that this script is attached to (and the this keyword is optional, so code could refer to gameObject without anything in front of it).
协程函数的第一行使对象翻倒。如第 2 章所述,旋转可以定义为围绕三个坐标轴 x、y 和 z 中的每一个的角度。因为我们不希望对象左右旋转,所以将 Y 和 Z 保留为0,并为 X 旋转分配一个角度。
The first line of the coroutine function makes the object tip over. As discussed in chapter 2, rotations can be defined as an angle around each of the three coordinate axes, x, y, and z. Because we don’t want the object to rotate side to side, leave Y and Z as 0 and assign an angle to the X rotation.
Figure 3.4 The target object falling over when hit
注意:变换会立即应用,但您可能更喜欢看到物体倾倒时的移动。一旦您开始在本书之外寻找更高级的主题,您可能想要查找补间,这是一种用于使物体随时间平滑移动的系统。
NOTE The transform is applied instantly, but you may prefer seeing the movement when objects topple over. Once you start looking beyond this book for more advanced topics, you might want to look up tweens, systems used to make objects move smoothly over time.
方法的第二行使用了对协程非常重要的yield关键字,在那里暂停函数并返回恢复之前要等待的秒数。最后,游戏对象在函数的最后一行中销毁自身。等待时间过后调用Destroy(this.gameObject) ,就像之前调用Destroy(sphere)的代码一样。
The second line of the method uses the yield keyword that’s so significant to coroutines, pausing the function there and returning the number of seconds to wait before resuming. Finally, the game object destroys itself in the last line of the function. Destroy(this.gameObject) is called after the wait time, just as the code called Destroy(sphere) before.
警告确保在this.gameObject上调用Destroy(),而不是简单地调用this!不要混淆这两者;this仅指此脚本组件,而this.gameObject指脚本附加到的对象。
WARNING Be sure to call Destroy() on this.gameObject and not simply this! Don’t get confused between the two; this refers only to this script component, whereas this.gameObject refers to the object the script is attached to.
目标现在对被射击有反应了——太棒了!但它本身没有做任何其他事情,所以让我们添加更多行为,使这个目标成为一个合适的敌人特点。
The target now reacts to being shot—great! But it doesn’t do anything else on its own, so let’s add more behavior to make this target a proper enemy character.
一个静态目标并不是很有趣,所以让我们编写代码让敌人四处游荡。四处游荡的代码几乎是人工智能 (AI) 或计算机控制实体最简单的例子。在这种情况下,实体是游戏中的敌人,但它也可能是现实世界中的机器人或下棋的声音。
A static target isn’t terribly interesting, so let’s write code that’ll make the enemy wander around. Code for wandering around is pretty much the simplest example of artificial intelligence (AI), or computer-controlled entities. In this case, the entity is an enemy in a game, but it could also be a robot in the real world or a voice that plays chess, for example.
多种的存在多种实现 AI 的方法(说真的,AI 是计算机科学家研究的主要领域)。为了我们的目的,我们将坚持使用一种简单的方法。随着您变得更加有经验并且您的游戏变得更加复杂,您可能想要探索实现 AI 的各种方法。
Multiple approaches to AI exist (seriously, AI is a major area of research for computer scientists). For our purposes, we’ll stick with a simple one. As you become more experienced and your games get more sophisticated, you’ll probably want to explore the various approaches to AI.
图 3.5 描述了基本过程。在每一帧中,AI 代码都会扫描其周围环境以确定是否需要做出反应。如果敌人的路上出现障碍物,敌人就会转向不同的方向。无论敌人是否需要转向,它都会始终稳步向前移动。因此,敌人会在房间内来回移动,始终向前移动并转向以避开墙壁。
Figure 3.5 depicts the basic process. In every frame, the AI code will scan around its environment to determine whether it needs to react. If an obstacle appears in its way, the enemy turns to face a different direction. Regardless of whether the enemy needs to turn, it will always move forward steadily. As such, the enemy will ping-pong around the room, always moving forward and turning to avoid walls.
Figure 3.5 Basic AI: cyclical process of moving forward and avoiding obstacles
代码看起来非常熟悉,因为它使用与玩家前进相同的命令来移动敌人。AI 代码也将使用射线投射,与以下类似,但上下文不同:射击。
The code will look pretty familiar, because it moves enemies forward by using the same commands as moving the player forward. The AI code will also use raycasting, similar to, but in a different context from, shooting.
作为正如您在本章的介绍中看到的,光线投射是一种可用于 3D 模拟中的多项任务的技术。一项容易掌握的任务是射击,但光线投射可用于的另一项任务是扫描场景。鉴于扫描场景是 AI 代码中的一个步骤,这意味着光线投射用于 AI 代码。
As you saw in the introduction to this chapter, raycasting is a technique that’s useful for multiple tasks within 3D simulations. One easily grasped task is shooting, but another task raycasting can be useful for is scanning around the scene. Given that scanning around the scene is a step in AI code, that means raycasting is used in AI code.
之前,您创建了一条源自相机的射线,因为那是玩家注视的位置。这次,您将创建一条源自敌人的射线。第一条射线从屏幕中心射出,但这次射线将向前射向角色前方;图 3.6 说明了这一点。然后,就像射击代码使用 RaycastHit信息来确定是否击中了任何东西以及击中了哪里一样,AI 代码将使用RaycastHit信息来确定敌人前方是否有东西,如果有,距离有多远。
Earlier, you created a ray that originated from the camera, because that’s where the player was looking from. This time, you’ll create a ray that originates from the enemy. The first ray shot out through the center of the screen, but this time the ray will shoot forward in front of the character; figure 3.6 illustrates this. Then, just as the shooting code used RaycastHit information to determine whether anything was hit and where, the AI code will use RaycastHit information to determine whether anything is in front of the enemy and, if so, how far away.
Figure 3.6 Using raycasting to “see” obstacles
射击的射线投射和 AI 的射线投射之间的一个区别是射线的半径。对于射击,射线被视为无限细,但对于 AI,射线将被视为具有较大的横截面。就代码而言,这意味着使用SphereCast()方法而不是Raycast()。造成这种差异的原因是子弹很小,而检查角色前方的障碍物则需要考虑角色的宽度。
One difference between raycasting for shooting and raycasting for AI is the radius of the ray. For shooting, the ray was treated as infinitely thin, but for AI, the ray will be treated as having a large cross section. In terms of the code, this means using the SphereCast()method instead of Raycast(). The reason for this difference is that bullets are tiny, whereas checking for obstacles in front of the character requires us to account for the width of the character.
创建一个名为WanderingAI的新脚本,将其附加到目标对象(与ReactiveTarget脚本一起),然后编写下一个清单中的代码。现在播放场景,您应该会看到敌人在房间里徘徊;您仍然可以射击目标,它会像以前一样做出反应。
Create a new script called WanderingAI, attach that to the target object (alongside the ReactiveTarget script), and write the code from the next listing. Play the scene now and you should see the enemy wandering around the room; you can still shoot the target, and it will react the same way as before.
Listing 3.7 Basic WanderingAI script
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 WanderingAI:MonoBehaviour {
公共浮动速度 = 3.0f; ❶
公共浮动障碍物范围 = 5.0f;
无效更新(){
变换.平移(0,0,速度*时间.deltaTime); ❷
Ray ray = new Ray(transform.position, transform.forward); ❸
RaycastHit 命中;
if (Physics.SphereCast(ray, 0.75f, out hit)) { ❹
如果 (hit.distance < 障碍物范围) {
浮动角度=随机范围(-110,110); ❺
变换。旋转(0,角度,0);
}
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WanderingAI : MonoBehaviour {
public float speed = 3.0f; ❶
public float obstacleRange = 5.0f;
void Update() {
transform.Translate(0, 0, speed * Time.deltaTime); ❷
Ray ray = new Ray(transform.position, transform.forward); ❸
RaycastHit hit;
if (Physics.SphereCast(ray, 0.75f, out hit)) { ❹
if (hit.distance < obstacleRange) {
float angle = Random.Range(-110, 110); ❺
transform.Rotate(0, angle, 0);
}
}
}
}
❶ Values for the speed of movement and the distance at which to react to obstacles
❷ Move forward continuously every frame, regardless of turning.
❸ A ray at the same position and pointing in the same direction as the character
❹ Perform raycasting with a circular volume around the ray.
❺ Turn toward a semi-random new direction.
此清单添加了几个变量来表示移动速度和 AI 对障碍物做出反应的距离。然后,在Update()方法中添加了transform.Translate()以连续向前移动(包括使用deltaTime进行与帧速率无关的移动)。在Update()中,您还会看到与之前的射击脚本非常相似的射线投射代码;同样,这里使用了相同的射线投射技术来观察而不是射击。射线是使用敌人的位置和方向而不是使用相机创建的。
This listing adds a couple of variables to represent the speed of movement and the distance at which the AI reacts to obstacles. Then, transform.Translate() is added in the Update() method to move forward continuously (including the use of deltaTime for frame rate-independent movement). In Update(), you’ll also see raycasting code that looks a lot like the shooting script from earlier; again, the same technique of raycasting is being used here to see instead of shoot. The ray is created using the enemy’s position and direction, instead of using the camera.
如前所述,光线投射计算是使用Physics.SphereCast()方法完成的。此方法采用 radius 参数来确定在射线周围多远的地方检测交点,但在其他方面,它与Physics.Raycast()完全相同。这种相似性包括命令如何填写命中信息、像以前一样检查交点,以及使用distance属性确保只有当敌人靠近障碍物(而不是房间对面的墙壁)时才做出反应。
As explained earlier, the raycasting calculation is done using the Physics.SphereCast() method. This method takes a radius parameter to determine how far around the ray to detect intersections, but in every other respect, it’s exactly the same as Physics.Raycast(). This similarity includes how the command fills in hit information, checks for intersections just as before, and uses the distance property to be sure to react only when the enemy gets near an obstacle (as opposed to a wall across the room).
当敌人前方有障碍物时,代码会将角色以半随机量向新方向旋转。我说半随机是因为这些值被限制为适合这种情况的最小值和最大值。具体来说,我们使用Random.Range()方法,Unity 提供此功能来获取约束之间的随机值。在本例中,约束略微超出了精确的左转或右转,允许角色进行足够的转向以避免障 碍。
When the enemy has a nearby obstacle right in front of it, the code rotates the character a semi-random amount toward a new direction. I say semi-random because the values are constrained to the minimum and maximum values that make sense for this situation. Specifically, we use the Random.Range() method, which Unity provides for obtaining a random value between constraints. In this case, the constraints were just slightly beyond an exact left or right turn, allowing the character to turn sufficiently to avoid obstacles.
一当前行为的一个奇怪之处是,敌人在被击中后倒下,仍然向前移动。这是因为,现在,Translate()方法无论如何,每帧都会运行。让我们对代码进行一些小的调整,以跟踪角色是否活着——或者用另一种(更技术性的)方式来说,我们想要跟踪角色的活着状态。
One oddity of the current behavior is that the enemy keeps moving forward after falling over from being hit. That’s because, right now, the Translate() method runs every frame no matter what. Let’s make small adjustments to the code to keep track of whether the character is alive—or to put it in another (more technical) way, we want to track the alive state of the character.
让代码跟踪对象的当前状态并做出不同的响应是许多编程领域(不仅仅是人工智能)的常见代码模式。这种方法的更复杂实现称为状态机,甚至可能是有限状态机。
Having the code keep track of and respond differently to the current state of the object is a common code pattern in many areas of programming, not just AI. More sophisticated implementations of this approach are referred to as state machines, or possibly even finite-state machines.
定义有限状态机(FSM)是一种代码结构,其中跟踪对象的当前状态,状态之间存在明确定义的转换,并且代码的行为根据状态而不同。
DEFINITION A finite-state machine (FSM) is a code structure in which the current state of the object is tracked, well-defined transitions exist between states, and the code behaves differently based on the state.
我们不会实现完整的 FSM,但在 AI 讨论中经常看到首字母缩写FSM,这并非巧合。完整的 FSM 会为复杂 AI 应用程序的多种行为提供多种状态,但在这个基本的 AI 中,我们只需要跟踪角色是否活着。下一个清单在脚本顶部添加了一个布尔值isAlive,代码需要偶尔对该值进行条件检查。有了这些检查,移动代码只会在敌人活着时运行。
We’re not going to implement a full FSM, but it’s no coincidence that a common place to see the initials FSM is in discussions of AI. A full FSM would have many states for the many behaviors of a sophisticated AI application, but in this basic AI, we need to track only whether the character is alive. The next listing adds a Boolean value, isAlive, toward the top of the script, and the code needs occasional conditional checks of that value. With those checks in place, the movement code runs only while the enemy is alive.
Listing 3.8 WanderingAI script with alive state added
... 私有 bool isAlive; ❶ 无效开始(){ isAlive = true; ❷ } 无效更新(){ 如果 (isAlive) { ❸ 变换.平移(0,0,速度*时间.deltaTime); ... } } 公共无效SetAlive(bool alive){ ❹ 是否还活着=还活着; } ...
... private bool isAlive; ❶ void Start() { isAlive = true; ❷ } void Update() { if (isAlive) { ❸ transform.Translate(0, 0, speed * Time.deltaTime); ... } } public void SetAlive(bool alive) { ❹ isAlive = alive; } ...
❶ Boolean value to track whether the enemy is alive
❸ Move only if the character is alive.
❹ Public method allowing outside code to affect the “alive” state
ReactiveTarget脚本现在可以告诉WanderingAI脚本敌人是活。
The ReactiveTarget script can now tell the WanderingAI script whether the enemy is alive.
清单 3.9 ReactiveTarget死亡时通知WanderingAI
Listing 3.9 ReactiveTarget tells WanderingAI when it dies
...
公共无效ReactToHit(){
WanderingAI 行为 = GetComponent<WanderingAI>();
if (行为 != null) { ❶
行为.SetAlive(false);
}
启动协同程序(Die());
}
......
public void ReactToHit() {
WanderingAI behavior = GetComponent<WanderingAI>();
if (behavior != null) { ❶
behavior.SetAlive(false);
}
StartCoroutine(Die());
}
...
❶ Check if this character has a WanderingAI script; it might not.
在此时,场景中只有一个敌人,当它死亡时,场景是空的。让我们让游戏生成敌人,这样每当敌人死亡时,就会出现一个新的敌人。这在 Unity 中很容易通过使用预制件来实现。
At the moment, only one enemy is in the scene, and when it dies, the scene is empty. Let’s make the game spawn enemies so that whenever the enemy dies, a new one appears. This is easily done in Unity by using prefabs.
预制件是一种灵活的可视化定义交互式对象的方法。简而言之,预制件是一个完全充实的游戏对象(已连接并设置好组件),它不存在于任何特定场景中,而是作为可复制到任何场景中的资产存在。
Prefabs are a flexible approach to visually defining interactive objects. In a nutshell, a prefab is a fully fleshed-out game object (with components already attached and set up) that doesn’t exist in any specific scene but rather exists as an asset that can be copied into any scene.
这种复制可以手动完成,以确保敌人对象(或其他预制件)在每个场景中都是相同的。但更重要的是,预制件也可以通过代码生成;您可以使用脚本中的命令将对象的副本放入场景中,而不仅仅是在可视化编辑器中手动执行此操作。
This copying can be done manually, to ensure that the enemy object (or other prefab) is the same in every scene. More importantly, though, prefabs can also be spawned from code; you can place copies of the object into the scene by using commands in scripts and not only by doing so manually in the visual editor.
定义资产是项目视图中显示的任何文件;这些文件可以是 2D 图像、3D 模型、代码文件、场景等等。我在第 1 章中简要提到过这个术语,但直到现在才强调它。
DEFINITION An asset is any file that shows up in the Project view; these could be 2D images, 3D models, code files, scenes, and so on. I mentioned this term briefly in chapter 1 but didn’t emphasize it until now.
预制件的副本称为实例,类似于实例,指代从类创建的特定代码对象。尽量保持术语的简洁性:预制件是指存在于任何场景之外的游戏对象;实例是指放置在场景。
A copy of a prefab is called an instance, analogous to instance referring to a specific code object created from a class. Try to keep the terminology straight: prefab refers to the game object existing outside of any scene; instance refers to a copy of the object that’s placed in a scene.
DEFINITION Also analogous to object-oriented terminology, instantiate is the action of creating an instance.
到创建预制件,首先在场景中创建一个将成为预制件的对象。因为我们的敌人对象将成为预制件,所以我们已经完成了第一步。现在我们要做的就是将对象从层次结构视图向下拖放到项目视图中;这将自动将对象保存为预制件(见图 3.7)。
To create a prefab, first create an object in the scene that will become the prefab. Because our enemy object will become a prefab, we’ve already done this first step. Now all we do is drag the object down from the Hierarchy view and drop it in the Project view; this will automatically save the object as a prefab (see figure 3.7).
Figure 3.7 Drag objects from Hierarchy to Project to create prefabs.
返回“层次结构”视图,原始对象的名称将变为蓝色,表示它现在已链接到预制件。我们实际上不再需要场景中的对象(我们将生成预制件,而不是使用场景中已有的实例),因此现在删除敌人对象。如果您想进一步编辑预制件,只需双击“项目”视图中的预制件以将其打开,然后单击“层次结构”视图左上角的后退箭头再次将其关闭。
Back in the Hierarchy view, the original object’s name will turn blue to signify that it’s now linked to a prefab. We don’t actually want the object in the scene anymore (we’re going to spawn the prefab, not use the instance already in the scene), so delete the enemy object now. If you want to edit the prefab further, just double-click the prefab in the Project view to open it and then click the back arrow at the top left of the Hierarchy view to close it again.
警告:与 Unity 早期版本相比,使用预制件的界面已有了很大改进,但编辑预制件仍然会造成混乱。例如,双击预制件后,从技术上讲您并不在任何场景中,因此请记住在编辑完预制件后单击层次结构视图中的后退箭头。此外,如果您嵌套预制件(以便一个预制件包含其他预制件),使用它们可能会造成混乱。
WARNING The interface for working with prefabs has improved a lot since earlier versions of Unity, but editing prefabs can still cause confusion. For example, you are not technically in any scene after you double-click a prefab, so remember to click the back arrow in the Hierarchy view when you are done editing the prefab. In addition, if you nest prefabs (so that one prefab contains other prefabs), working with them can get confusing.
现在我们有了在场景中生成的实际预制对象,所以让我们编写代码来创建预制。
Now we have the actual prefab object to spawn in the scene, so let’s write code to create instances of the prefab.
虽然预制件本身不存在于场景中,必须有一个对象存在于场景中,敌人生成代码才能附加到该对象上。我们将创建一个空的游戏对象,并将脚本附加到该对象上,但该对象在场景中不可见。
Although the prefab itself doesn’t exist in the scene, an object must be in the scene for the enemy spawning code to attach to. We’ll create an empty game object and can attach the script to that, but the object won’t be visible in the scene.
提示使用空游戏对象来附加脚本组件是 Unity 开发中的常见模式。此技巧用于不适用于场景中任何特定对象的抽象任务。Unity 脚本旨在附加到可见对象,但并非每个任务都有意义。
TIP The use of empty GameObjects for attaching script components is a common pattern in Unity development. This trick is used for abstract tasks that don’t apply to any specific object in the scene. Unity scripts are intended to be attached to visible objects, but not every task makes sense that way.
选择 GameObject > Create Empty,将新对象重命名为Controller,并确保其位置为0,0,0 。 (从技术上讲,位置并不重要,因为对象不可见,但如果您将其作为父对象,则将其放在原点将使生活更简单。)创建一个名为SceneController的脚本。
Choose GameObject > Create Empty, rename the new object Controller, and ensure that its position is 0, 0, 0. (Technically, the position doesn’t matter because the object isn’t visible, but putting it at the origin will make life simpler if you ever parent anything to it.) Create a script called SceneController.
清单 3.10生成敌人预制件的SceneController
Listing 3.10 SceneController that spawns the enemy prefab
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 SceneController : MonoBehaviour {
[SerializeField] GameObject enemyPrefab; ❶
private GameObject enemy; ❷
无效更新(){
if (enemy == null) { ❸
enemy = Instantiate(enemyPrefab) as GameObject; ❹
敌人.变换.位置 = 新 Vector3(0, 1, 0);
浮动角度 = 随机范围(0, 360);
敌人.变换.旋转(0,角度,0);
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SceneController : MonoBehaviour {
[SerializeField] GameObject enemyPrefab; ❶
private GameObject enemy; ❷
void Update() {
if (enemy == null) { ❸
enemy = Instantiate(enemyPrefab) as GameObject; ❹
enemy.transform.position = new Vector3(0, 1, 0);
float angle = Random.Range(0, 360);
enemy.transform.Rotate(0, angle, 0);
}
}
}
❶ Serialized variable for linking to the prefab object
❷ Private variable to keep track of the enemy instance in the scene
❸ Spawn a new enemy only if one isn’t already in the scene.
❹ Method that copies the prefab object
将此脚本附加到控制器对象,在 Inspector 中您将看到敌人预制件的变量槽。其工作原理与公共变量类似,但有一个重要区别。
Attach this script to the controller object, and in the Inspector you’ll see a variable slot for the enemy prefab. This works similarly to public variables, but there’s an important difference.
提示若要在 Unity 编辑器中引用对象,我建议使用SerializeField修饰变量,而不是将其声明为公共变量。如第 2 章所述,公共变量显示在 Inspector 中(换句话说,它们由 Unity 序列化),因此您将看到的大多数教程和示例代码都对所有序列化值使用公共变量。但这些变量也可以由其他脚本修改(毕竟这些是公共变量),而SerializeField属性允许您将变量保持为私有。如果变量未明确公开,则 C# 默认为私有,这在大多数情况下是更好的选择,因为您希望在 Inspector 中公开该变量,但不希望其他脚本更改其值。
TIP To reference objects in Unity’s editor, I recommend decorating variables with SerializeField instead of declaring them to be public. As explained in chapter 2, public variables show up in the Inspector (in other words, they’re serialized by Unity), so most tutorials and sample code you’ll see use public variables for all serialized values. But these variables can also be modified by other scripts (these are public variables, after all), whereas the SerializeField attribute allows you to keep the variables private. C# defaults to private if a variable isn’t explicitly made public, and that’s better in most cases because you want to expose that variable in the Inspector but don’t want the value to be changed by other scripts.
警告在 2019.4 版之前,Unity 有一个错误,SerializeField会导致编译器发出有关该字段未初始化的警告。如果您遇到此错误,脚本仍可正常运行,因此从技术上讲,您可以忽略这些警告,或者通过向这些字段添加= null来消除它们。
WARNING Prior to version 2019.4, Unity had a bug in which SerializeField would cause the compiler to emit a warning about that field not being initialized. If you ever encounter this bug, the script still functions fine, so technically you can just ignore those warnings or get rid of them by adding = null to those fields.
将预制资产从 Project 拖到空变量槽中。当鼠标靠近时,您应该看到槽突出显示,表明该对象可以链接到那里(见图 3.8)。一旦敌人预制件链接到SceneController脚本,播放场景以查看代码的实际效果。敌人会像以前一样出现在房间中间,但现在如果你射击敌人,它将被新的敌人取代。这比一个永远消失的敌人要好得多!
Drag the prefab asset up from Project to the empty variable slot. When the mouse gets near, you should see the slot highlight to indicate that the object can be linked there (see figure 3.8). Once the enemy prefab is linked to the SceneController script, play the scene to see the code in action. An enemy will appear in the middle of the room just as before, but now if you shoot the enemy, it will be replaced by a new enemy. That’s much better than just one enemy that’s gone forever!
Figure 3.8 Link the enemy prefab to the script’s prefab slot.
提示将对象拖到 Inspector 的变量槽上是一种方便的技术,在很多脚本中都会用到。在这里,我们将一个预制件链接到脚本,但您也可以链接到场景中的对象,甚至可以链接到特定组件(而不是整个 GameObject)。在以后的章节中,我们将经常使用这种技术。
TIP This approach of dragging objects onto the Inspector’s variable slots is a handy technique that comes up in a lot of scripts. Here we linked a prefab to the script, but you can also link to objects in the scene and can even link to specific components (rather than the overall GameObject). In future chapters, we’ll use this technique often.
该脚本的核心是Instantiate()方法,所以请注意那行。当我们实例化预制件时,会在场景中创建一个副本。默认情况下,Instantiate()将新对象作为通用Object类型返回。但是Object直接使用是没什么用的,我们需要将其作为GameObject来处理。在 C# 中,使用as关键字进行类型转换,将一种类型的代码对象转换为另一种类型(使用语法original-object as new-type 编写)。
The core of this script is the Instantiate() method, so take note of that line. When we instantiate the prefab, that creates a copy in the scene. By default, Instantiate() returns the new object as a generic Object type, but Object is pretty useless directly, and we need to handle it as a GameObject. In C#, use the as keyword for typecasting to convert from one type of code object into another type (written with the syntax original-object as new-type).
实例化的对象存储在enemy中,这是GameObject类型的私有变量(保持预制件和预制件实例之间的区别:enemyPrefab存储预制件;enemy存储实例。)if语句检查存储的对象可确保仅当enemy为空(或按编码人员的说法为 null )时才调用Instantiate() 。变量从空开始,因此实例化代码从会话一开始就运行一次。然后Instantiate()返回的对象存储在enemy中,这样实例化代码就不会再次运行。
The instantiated object is stored in enemy, a private variable of the GameObject type. (Keep the distinction between a prefab and an instance of the prefab straight: enemyPrefab stores the prefab; enemy stores the instance.) The if statement that checks the stored object ensures that Instantiate() is called only when enemy is empty (or null, in coder-speak). The variable starts out empty, so the instantiating code runs once right from the beginning of the session. The object returned by Instantiate() is then stored in enemy so that the instantiating code won’t run again.
因为敌人被击中后会自我毁灭,所以敌人变量会被清空,并导致Instantiate()再次运行。这样,敌人就会一直处于这场景。
Because the enemy destroys itself when shot, that empties the enemy variable and causes Instantiate() to be run again. In this way, an enemy is always in the scene.
全部好了,让我们为敌人添加一些功能。就像我们对玩家所做的那样,首先我们让他们移动——现在让我们让他们射击!正如我在介绍光线投射时提到的,那只是实现射击的方法之一。另一种方法涉及实例化预制件,所以让我们采用这种方法让敌人反击。本节的目标是在玩游戏时看到图 3.9。
All right, let’s add another bit of functionality to the enemies. Much as we did with the player, first we made them move—now let’s make them shoot! As I mentioned back when introducing raycasting, that was just one of the approaches to implementing shooting. Another approach involves instantiating prefabs, so let’s take that approach to making the enemies shoot back. The goal of this section is to see figure 3.9 when playing.
Figure 3.9 Enemy shooting a fireball at the player.
这以前,射击会涉及场景中的射弹。使用射线射击基本上是瞬时的,在单击鼠标的那一刻记录命中,但这次敌人会发射飞过空中的火球。诚然,他们的移动速度会非常快,但不是瞬时的,这让玩家有机会躲开。我们将使用碰撞检测(与防止移动玩家穿过墙壁的碰撞系统相同),而不是使用射线来检测命中。
This time, shooting will involve a projectile in the scene. Shooting with raycasting was basically instantaneous, registering a hit the moment the mouse was clicked, but this time enemies are going to emit fireballs that fly through the air. Admittedly, they’ll be moving pretty fast, but not instantaneously, giving the player a chance to dodge out of the way. Instead of using raycasting to detect hits, we’ll use collision detection (the same collision system that keeps the moving player from passing through walls).
代码将以与敌人生成相同的方式生成火球:通过实例化预制件。如上一节所述,创建预制件的第一步是在场景中创建一个将成为预制件的对象,因此让我们创建一个火球。
The code will spawn fireballs in the same way that enemies spawn: by instantiating a prefab. As explained in the previous section, the first step when creating a prefab is to create an object in the scene that will become the prefab, so let’s create a fireball.
首先,选择 GameObject > 3D Object > Sphere。将新对象重命名为Fireball。现在创建一个新脚本,也称为Fireball ,并将该脚本附加到此对象。最终,我们将在此脚本中编写代码,但现在将其保留为默认脚本,同时我们将处理Fireball对象的其他几个部分。为了使它看起来像一个火球,而不仅仅是一个灰色的球体,我们将给该物体涂上明亮的橙色。颜色等表面属性由材料控制。
To start, choose GameObject > 3D Object > Sphere. Rename the new object Fireball. Now create a new script, also called Fireball, and attach that script to this object. Eventually, we’ll write code in this script, but leave it as the default for now while we work on a few other parts of the Fireball object. So that it appears like a fireball and not just a gray sphere, we’re going to give the object a bright orange color. Surface properties such as color are controlled using materials.
定义材料是一包信息,用于定义材质所附着的任何 3D 对象的表面属性。这些表面属性可以包括颜色、光泽度,甚至细微的粗糙度。
DEFINITION A material is a packet of information that defines the surface properties of any 3D object that the material is attached to. These surface properties can include color, shininess, and even subtle roughness.
选择 Assets > Create > Material。将新材质命名为Flame之类的名称,然后将其拖到场景中的对象上。在 Project 视图中选择材质,以便在 Inspector 中查看材质的属性。如图 3.10 所示,单击标有 Albedo 的色板(这是一个技术术语,指表面的主色)。单击该色板将在自己的窗口中打开颜色选择器;滑动彩虹色环和主选择区域可将颜色设置为橙色。
Choose Assets > Create > Material. Name the new material something like Flame and drag it onto the object in the scene. Select the material in the Project view in order to see the material’s properties in the Inspector. As figure 3.10 shows, click the color swatch labeled Albedo (that’s a technical term that refers to the main color of a surface). Clicking that will bring up a color picker in its own window; slide both the rainbow-colored ring and the main picking area to set the color to orange.
Figure 3.10 Setting the color of a material
我们还将使材质变亮,使其看起来更像火焰。调整 Emission 值(检查器中的其他属性之一)。默认情况下,复选框处于关闭状态,因此请将其打开以使材质变亮。
We’re also going to brighten the material to make it look more like fire. Adjust the Emission value (one of the other attributes in the Inspector). The check box is off by default, so turn it on to brighten up the material.
现在,您可以将火球对象从层次结构拖到项目中,将其变成预制件,就像您对敌人预制件所做的那样。与敌人一样,我们现在只需要预制件,因此删除层次结构中的实例。太好了 - 我们有一个新的预制件可以用作射弹!接下来是编写代码以使用该预制件进行射击。射彈。
Now you can turn the fireball object into a prefab by dragging the object down from Hierarchy into Project, just as you did with the enemy prefab. As with the enemy, we need only the prefab now, so delete the instance in the Hierarchy. Great—we have a new prefab to use as a projectile! Next up is writing code to shoot using that projectile.
让我们对敌人进行调整以发射火球。由于识别玩家的代码需要新脚本(就像代码需要ReactiveTarget来识别目标一样),因此首先创建一个新脚本并将其命名为PlayerCharacter。将此脚本附加到场景中的玩家对象。现在打开WanderingAI并从此清单中添加代码。
Let’s make adjustments to the enemy in order to emit fireballs. Because code to recognize the player will require a new script (just like ReactiveTarget was required by the code to recognize the target), first create a new script and name it PlayerCharacter. Attach this script to the player object in the scene. Now open up WanderingAI and add to the code from this listing.
Listing 3.11 WanderingAI additions for emitting fireballs
... [SerializeField] GameObject fireballPrefab; ❶ 私人游戏对象火球; ... 如果 (Physics.SphereCast(ray, 0.75f, out hit)) { 游戏对象 hitObject = hit.transform.gameObject; 如果 (hitObject.GetComponent<PlayerCharacter>()) { ❷ 如果 (fireball == null) { ❸ fireball = Instantiate(fireballPrefab) as GameObject; ❹ 火球.变换.位置 = 变换.TransformPoint(Vector3.forward * 1.5f); ❺ 火球.变换.旋转 = 变换.旋转; } } 否则,如果 (hit.distance < 障碍物范围) { 浮动角度 = 随机范围(-110, 110); 变换。旋转(0,角度,0); } } ...
... [SerializeField] GameObject fireballPrefab; ❶ private GameObject fireball; ... if (Physics.SphereCast(ray, 0.75f, out hit)) { GameObject hitObject = hit.transform.gameObject; if (hitObject.GetComponent<PlayerCharacter>()) { ❷ if (fireball == null) { ❸ fireball = Instantiate(fireballPrefab) as GameObject; ❹ fireball.transform.position = transform.TransformPoint(Vector3.forward * 1.5f); ❺ fireball.transform.rotation = transform.rotation; } } else if (hit.distance < obstacleRange) { float angle = Random.Range(-110, 110); transform.Rotate(0, angle, 0); } } ...
❶在任何方法之前添加这两个字段,就像在SceneController中一样。
❶ Add these two fields before any methods, just as in SceneController.
❷玩家的检测方式与 RayShooter 中目标对象的检测方式相同。
❷ Player is detected in the same way as the target object in RayShooter.
❸与 SceneController 相同的空 Game-Object 逻辑
❸ Same null Game-Object logic as SceneController
❹这里的 Instantiate() 方法与 SceneController 中的一样。
❹ Instantiate() method here is just as it was in SceneController.
❺ Place the fireball in front of the enemy and point in the same direction.
您会注意到,此清单中的所有注释都引用了先前脚本中类似(或相同)的部分。先前的代码清单显示了发射火球所需的一切;现在我们将代码片段混合在一起并重新混合以适应新的上下文。
You’ll notice that all the annotations in this listing refer to similar (or the same) bits in previous scripts. Previous code listings showed everything needed for emitting fireballs; now we’re mashing together and remixing bits of code to fit in the new context.
就像在SceneController中一样,您需要在脚本顶部添加两个GameObject字段:一个用于链接预制件的序列化变量和一个用于跟踪代码创建的实例的私有变量。进行射线投射后,代码会检查被击中的对象上的PlayerCharacter;这就像射击代码检查被击中的对象上的ReactiveTarget一样。当场景中还没有火球时实例化火球的代码就像实例化敌人的代码一样。不过,定位和旋转是不同的;这一次,您将实例放在敌人前面并将其指向同一方向。
Just as in SceneController, you need to add two GameObject fields toward the top of the script: a serialized variable for linking the prefab to, and a private variable for keeping track of the instance created by the code. After doing a raycast, the code checks for the PlayerCharacter on the object hit; this works just as the shooting code checking for ReactiveTarget on the object hit. The code that instantiates a fireball when there isn’t already one in the scene works like the code that instantiates an enemy. The positioning and rotation are different, though; this time, you place the instance just in front of the enemy and point it in the same direction.
一旦所有新代码都到位,当你选择 Enemy 预制件时,Inspector 中就会出现一个新的 Fireball Prefab 插槽,就像Scene-Controller组件中的 Enemy Prefab 插槽一样。单击 Project 视图中的 Enemy 预制件(双击才能真正打开预制件,但只需单击一下即可选择它),Inspector 将显示该对象的组件,就像你选择了场景中的对象一样。虽然之前关于界面尴尬的警告通常适用于编辑预制件,但界面可以轻松调整预制件上的组件而无需打开它,这就是我们所做的一切。如图 3.11 所示,将 Fireball 预制件从 Project 拖到 Inspector 中的 Fireball Prefab 插槽上(同样,就像你对SceneController所做的那样)。
Once all the new code is in place, a new Fireball Prefab slot will appear in the Inspector when you select the Enemy prefab, like the Enemy Prefab slot in the Scene-Controller component. Click the Enemy prefab in the Project view (double-click to actually open the prefab, but just a single click selects it), and the Inspector will show that object’s components, as if you’d selected an object in the scene. Although the earlier warning about interface awkwardness often applies when editing prefabs, the interface makes it easy to adjust the components on a prefab without opening it, and that’s all we’re doing. As shown in figure 3.11, drag the Fireball prefab from Project onto the Fireball Prefab slot in the Inspector (again, just as you did with SceneController).
Figure 3.11 Link the fireball prefab to the script’s prefab slot.
现在,当玩家位于敌人正前方时,敌人会向玩家开火……好吧,试着开火。明亮的橙色球体出现在敌人面前,但只是停在那里,因为我们还没有编写它的脚本。我们现在就开始吧。
Now the enemy will fire at the player when the player is directly ahead of it . . . well, try to fire. The bright orange sphere appears in front of the enemy but just sits there because we haven’t written its script yet. Let’s do that now.
Listing 3.12 Fireball script that reacts to collisions
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 Fireball : MonoBehaviour {
公共浮动速度=10.0f;
公共 int 损坏 = 1;
无效更新(){
变换.平移(0,0,速度*时间.deltaTime); ❶
}
void OnTriggerEnter(Collider 其他) { ❷
PlayerCharacter 玩家 = other.GetComponent<PlayerCharacter>();
如果 (玩家 != null) { ❸
Debug.Log("玩家命中");
}
销毁(this.gameObject);
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Fireball : MonoBehaviour {
public float speed = 10.0f;
public int damage = 1;
void Update() {
transform.Translate(0, 0, speed * Time.deltaTime); ❶
}
void OnTriggerEnter(Collider other) { ❷
PlayerCharacter player = other.GetComponent<PlayerCharacter>();
if (player != null) { ❸
Debug.Log("Player hit");
}
Destroy(this.gameObject);
}
}
❶ Move forward in the direction it faces.
❷ Called when another object collides with this trigger
❸ Check if the other object is a PlayerCharacter.
此代码的关键新部分是OnTriggerEnter()方法,当对象发生碰撞(例如与墙壁或玩家发生碰撞)时自动调用。目前,此代码无法完全工作;如果运行它,火球将由于Translate()行而向前飞行,但触发器不会运行,而是通过摧毁当前火球来排队新的火球。需要对Fireball对象上的组件进行一些其他调整。第一个更改是将对撞机设为触发器。要进行调整,请转到 Inspector 并单击 Sphere Collider 组件中的 Is Trigger 复选框。
The crucial new bit to this code is the OnTriggerEnter() method, called automatically when the object has a collision, such as with the walls or with the player. At the moment, this code won’t work entirely; if you run it, the fireball will fly forward thanks to the Translate() line, but the trigger won’t run, queuing up a new fireball by destroying the current one. A couple of other adjustments need to be made to components on the Fireball object. The first change is making the collider a trigger. To adjust that, go to the Inspector and click the Is Trigger check box in the Sphere Collider component.
提示:设置为触发器的对撞机组件仍会对接触/重叠的其他物体做出反应,但不再阻止其他物体物理通过。
TIP A collider component set as a trigger will still react to touching/overlapping other objects but will no longer stop other objects from physically passing through.
火球还需要一个 Rigidbody,这是 Unity 中物理系统使用的组件。通过为火球提供一个 Rigidbody 组件,可以确保物理系统能够为该对象注册碰撞触发器。单击检查器底部的“添加组件”,然后选择“物理”>“Rigidbody”。在添加的组件中,取消选择“使用重力”(见图 3.12),这样火球就不会被重力拉下。
The fireball also needs a Rigidbody, a component used by the physics system in Unity. By giving the fireball a Rigidbody component, you ensure that the physics system is able to register collision triggers for that object. Click Add Component at the bottom of the Inspector and choose Physics > Rigidbody. In the component that’s added, deselect Use Gravity (see figure 3.12) so that the fireball won’t be pulled down by gravity.
Figure 3.12 Turn off gravity in the Rigidbody component.
现在开始游戏,火球击中某物时会被摧毁。由于只要场景中没有火球,发射火球的代码就会运行,所以敌人会向玩家发射更多火球。现在,向玩家射击只剩下一件事:让玩家对被打。
Play now, and fireballs are destroyed when they hit something. Because the fireball-emitting code runs whenever a fireball isn’t already in the scene, the enemy will shoot more fireballs at the player. Now just one more thing remains for shooting at the player: making the player react to being hit.
此前,你创建了一个PlayerCharacter脚本但将其留空。现在您将编写代码来让玩家对被击中做出反应。
Earlier, you created a PlayerCharacter script but left it empty. Now you’ll write code to have the player react to being hit.
Listing 3.13 Player that can take damage
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 PlayerCharacter:MonoBehaviour {
私人国际健康;
无效开始(){
健康=5; ❶
}
公共无效伤害(int伤害){
健康 - = 伤害; ❷
Debug.Log($"健康:{健康}"); ❸
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerCharacter : MonoBehaviour {
private int health;
void Start() {
health = 5; ❶
}
public void Hurt(int damage) {
health -= damage; ❷
Debug.Log($"Health: {health}"); ❸
}
}
❶ Initialize the health value.
❷ Decrement the player’s health.
❸ Construct the message by using string interpolation.
列表定义了玩家健康值字段,并根据命令减少健康值。在后面的章节中,我们将介绍文本显示以在屏幕上显示信息,但现在,我们只能使用调试消息来显示有关玩家健康值的信息。
The listing defines a field for the player’s health and reduces the health on command. In later chapters, we’ll go over text displays to show information on the screen, but for now, we can display information about the player’s health only by using debug messages.
定义 字符串插值是一种将代码求值(例如变量的值)插入字符串的机制。有几种编程语言支持字符串插值,包括 C#。例如,查看清单 3.13 中的健康信息。
DEFINITION String interpolation is a mechanism to insert the evaluation of code (for example, the value of a variable) into a string. Several programming languages support string interpolation, including C#. For example, look at the health message in listing 3.13.
现在你需要回到Fireball脚本来调用玩家的Hurt()方法. 将Fireball脚本中的调试行替换为player.Hurt(damage),以告知玩家他们已被击中。这就是我们需要的最后一段代码!
Now you need to go back to the Fireball script to call the player’s Hurt() method. Replace the debug line in the Fireball script with player.Hurt(damage) to tell the player they’ve been hit. And that’s the final bit of code we need!
呼!这一章内容非常丰富,引入了大量代码。将上一章与本章结合起来,您现在已经掌握了第一人称射手。
Whew! That was a pretty intense chapter, with lots of code introduced. Combining the previous chapter with this one, you now have most of the functionality in place for a first-person shooter.
我们主要关注游戏的功能,而不是游戏的外观。这并非偶然——本书主要讲述使用 Unity 进行游戏编程。不过,了解如何处理和改进视觉效果仍然很重要。在我们回到本书对游戏各个部分进行编码的主要关注点之前,让我们花一章来学习游戏艺术,这样您的项目就不会总是以空白框到处滑动而告终。
We’ve been focusing mostly on how the game functions and not as much on how the game looks. That was no accident—this book is mostly about programming games in Unity. Still, it’s important to understand how to work on and improve the visuals. Before we get back to the book’s main focus on coding various parts of the game, let’s spend a chapter learning about game art so that your projects won’t always end up with just blank boxes sliding around.
游戏中的所有视觉内容均由艺术资产组成。但这到底意味着什么呢?
All of the visual content in a game is made up of art assets. But what exactly does that mean?
一个艺术资产是游戏使用的视觉信息(通常是文件)的单独单元。这个总括性术语适用于所有视觉内容:图像文件是艺术资产,3D 模型是艺术资产,等等。事实上,艺术资产只是一种特定类型的资产,您已经了解到它是游戏使用的任何文件(例如脚本)——因此是 Unity 中的主 Assets 文件夹。表 4.1 描述了构建游戏使用的五种主要艺术资产。
An art asset is an individual unit of visual information (usually a file) used by the game. This overarching umbrella term applies to all visual content: image files are art assets, 3D models are art assets, and so on. Indeed, an art asset is simply a specific type of asset, which you’ve learned is any file used by the game (such as a script)—hence the main Assets folder in Unity. Table 4.1 describes the five main kinds of art assets used in building a game.
为新游戏创作艺术作品通常从 2D 图像或 3D 模型开始,因为这些资产构成了其他一切的基础。顾名思义,2D 图像是 2D 图形的基础,而3D 模型是 3D 图形的基础。具体来说,2D 图像是平面图片。即使您之前不熟悉游戏艺术,您可能已经熟悉了网站上使用的 2D 图像;另一方面,3D 模型可能需要为新手定义。
Creating art for a new game generally starts with either 2D images or 3D models because those assets form a base on which everything else relies. As the names imply, 2D images are the foundation of 2D graphics, whereas 3D models are the foundation of 3D graphics. Specifically, 2D images are flat pictures. Even if you have no previous familiarity with game art, you’re probably already familiar with 2D images from the graphics used on websites; 3D models, on the other hand, may need to be defined for a newcomer.
定义模型是一个 3D 虚拟对象。第 1 章介绍了术语“网格对象”,而3D 模型实际上是同义词。这两个术语经常互换使用,但网格对象严格指 3D 对象的几何形状(连接的线和形状),而模型则有点模糊,通常包括对象的其他属性。
DEFINITION A model is a 3D virtual object. Chapter 1 introduced the term mesh object, and 3D model is practically a synonym. The terms are frequently used interchangeably, but mesh object strictly refers to the geometry of the 3D object (the connected lines and shapes), whereas model is a bit more ambiguous and often includes other attributes of the object.
列表中接下来的两种资产类型是材质和动画。与 2D 图像和 3D 模型不同,材质和动画不会单独执行任何操作,而且新手很难理解。2D 图像和 3D 模型很容易通过现实世界的类似物来理解:前者是绘画,后者是雕塑。材质和动画与现实世界没有直接关系。相反,两者都是分层到 3D 模型上的抽象信息包。事实上,材质已经在第 3 章中进行了基本介绍。
The next two types of assets on the list are materials and animations. Unlike 2D images and 3D models, materials and animations don’t do anything in isolation and are much harder for newcomers to understand. 2D images and 3D models are easily understood through real-world analogs: paintings for the former, sculptures for the latter. Materials and animations aren’t as directly relatable to the real world. Instead, both are abstract packets of information that layer onto 3D models. In fact, materials were already introduced in a basic sense in chapter 3.
定义材料是定义其所附着的任何对象的表面属性(颜色、光泽度等)的信息包。单独定义表面属性可使多个对象共享一种材质(例如,所有城堡墙壁)。
DEFINITION A material is a packet of information that defines the surface properties (color, shininess, and so forth) of any object that it’s attached to. Defining surface properties separately enables multiple objects to share a material (all the castle walls, for example).
继续用艺术来类比,你可以将材料视为雕塑的制作媒介(粘土、黄铜、大理石等)。同样,动画也是附加在可见物体上的抽象信息层。
Continuing the art analogy, you can think of a material as the medium (clay, brass, marble, and so on) that the sculpture is made of. Similarly, an animation is also an abstract layer of information that’s attached to a visible object.
定义动画是定义相关对象运动的信息包。由于这些运动可以独立于对象本身定义,因此可以以混合搭配的方式与多个对象一起使用。
DEFINITION An animation is a packet of information that defines the movement of the associated object. Because these movements can be defined independently from the object itself, they can be used in a mix-and-match way with multiple objects.
举一个具体的例子,想象一下一个角色四处走动。角色的整体位置由游戏代码处理(例如,您在第 2 章中编写的运动脚本)。但脚着地、手臂摆动和臀部旋转等详细动作是正在播放的动画序列;该动画序列是艺术资产。
For a concrete example, think about a character walking around. The overall position of the character is handled by the game’s code (for example, the movement scripts you wrote in chapter 2). But the detailed movements of feet hitting the ground, arms swinging, and hips rotating are an animation sequence that’s being played back; that animation sequence is an art asset.
为了帮助您理解动画和 3D 模型之间的关系,我们将其与木偶戏进行类比:3D 模型是木偶,动画师是操纵木偶的木偶师,动画是木偶动作的记录。以这种方式定义的动作是提前创建的,通常是小规模的动作,不会改变对象的整体定位。这与前几章中用代码完成的大规模动作形成了对比。
To help you understand how animations and 3D models relate, let’s make an analogy with puppeteering: the 3D model is the puppet, the animator is the puppeteer who makes the puppet move, and the animation is a recording of the puppet’s movements. The movements defined this way are created ahead of time and are usually small-scale movements that don’t change the overall positioning of the object. This is in contrast to the sort of large-scale movements that were done in code in previous chapters.
表 4.1 中的最后一类艺术资产是粒子系统。粒子系统可用于创建视觉效果,如火焰、烟雾或喷水。
The final kind of art asset from table 4.1 is a particle system. Particle systems are useful for creating visual effects, like fire, smoke, or spraying water.
定义粒子系统是一种有序的机制,用于创建和控制大量移动物体。这些移动物体通常很小(因此得名粒子),但它们不一定如此。
DEFINITION A particle system is an orderly mechanism for creating and controlling large numbers of moving objects. These moving objects are usually small—hence the name particle—but they don’t have to be.
粒子(粒子系统控制下的单个对象)可以是您选择的任何网格对象。但对于大多数效果,粒子将是显示图片的正方形(例如火焰火花或烟雾)。
The particles (the individual objects under the control of a particle system) can be any mesh object that you choose. But for most effects, the particles will be a square displaying a picture (a flame spark or a smoke puff, for example).
游戏艺术创作的大部分工作都是在外部软件中完成的,而不是在 Unity 本身中。材质和粒子系统是在 Unity 中创建的,但其他艺术资产是使用外部软件创建的。请参阅附录 B 了解有关外部工具的更多信息;各种艺术应用程序都用于创建 3D 模型和动画。在外部工具中创建的 3D 模型随后被保存为 Unity 导入的艺术资产。我在附录 C 中解释如何建模时使用了 Blender(从www.blender.org下载),但这只是因为 Blender 是开源的,因此可供所有读者使用。
Much of the work of creating game art is done in external software, not within Unity itself. Materials and particle systems are created within Unity, but the other art assets are created using external software. Refer to appendix B to learn more about external tools; a variety of art applications are used for creating 3D models and animation. 3D models created in an external tool are then saved as an art asset that’s imported by Unity. I use Blender when explaining how to model in appendix C (download it from www.blender.org), but that’s just because Blender is open source and thus available to all readers.
注意本章的项目下载包含一个名为scratch的文件夹。尽管该文件夹与 Unity 项目位于同一位置,但它不是 Unity 项目的一部分;那是我放置额外外部文件的地方。
NOTE The project download for this chapter includes a folder named scratch. Although that folder is in the same place as the Unity project, it’s not part of the Unity project; that’s where I put extra external files.
在完成本章的项目时,您将看到大多数此类艺术资产的示例(动画目前有点太复杂,将在本书后面介绍)。您将构建一个使用 2D 图像、3D 模型、材质和粒子系统的场景。在某些情况下,您将引入现有的艺术资产并学习如何将它们导入 Unity,但在其他时候(尤其是使用粒子系统),您将在 Unity 中从头开始创建艺术资产。
As you work through the project for this chapter, you’ll see examples of most of these types of art assets (animations are a bit too complex for now and are addressed later in the book). You’re going to build a scene that uses 2D images, 3D models, materials, and a particle system. In some cases, you’ll bring in already existing art assets and learn how to import them into Unity, but at other times (especially with the particle system), you’ll create the art asset from scratch within Unity.
本章仅涉及游戏艺术创作的皮毛。由于本书重点介绍如何在 Unity 中编程,因此对艺术学科的广泛介绍会减少本书的覆盖范围。创作游戏艺术本身就是一个巨大的话题,很容易就能写满几本书。在大多数情况下,游戏程序员需要与专门从事该学科的游戏艺术家合作。话虽如此,对于游戏程序员来说,了解 Unity 如何处理艺术资产,甚至可能创建自己的粗略替身以供以后替换,是非常有用的(俗称程序员艺术)。
This chapter only scratches the surface of game art creation. Because this book focuses on how to program in Unity, extensive coverage of art disciplines would reduce how much the book could cover. Creating game art is a giant topic in and of itself, easily able to fill several books. In most cases, a game programmer would need to partner with a game artist who specializes in that discipline. That said, it’s extremely useful for game programmers to understand how Unity works with art assets and possibly even create their own rough stand-ins to be replaced later (commonly known as programmer art).
注意本章中没有任何内容直接需要前几章中的项目。但您需要有像第 2 章中的移动脚本,以便您可以在要构建的场景中走动。如有必要,您可以从项目下载中获取玩家对象和脚本。同样,本章以与前几章中创建的移动对象类似的移动对象结束。
NOTE Nothing in this chapter directly requires projects from the previous chapters. But you’ll want to have movement scripts like the ones from chapter 2 so that you can walk around the scene you’ll build. If necessary, you can grab the player object and scripts from the project download. Similarly, this chapter ends with moving objects that are similar to the ones created in previous chapters.
这我们将要讨论的第一个内容创建主题是白盒化。此过程通常是在计算机上构建关卡的第一步(在纸上设计关卡之后)。顾名思义,您使用空白几何体(白盒)遮挡场景的墙壁。查看表 4.1 中的艺术资产列表,此空白场景是最基本的 3D 模型,它为显示 2D 图像提供了基础。
The first content creation topic we’ll go over is whiteboxing. This process is usually the first step in building a level on the computer (after designing the level on paper). As the name suggests, you block out the walls of the scene with blank geometry (white boxes). Looking at the list of art assets in table 4.1, this blank scenery is the most basic sort of 3D model, and it provides a base on which to display 2D images.
如果您回想一下您在第 2 章中创建的原始场景,那基本上就是白盒化(您只是还没有学会这个术语)。本节的部分内容将重新讨论第 2 章开头所做的工作,但这次我们将更快地介绍该过程,并讨论更多新术语。
If you think back to the primitive scene you created in chapter 2, that was basically whiteboxing (you just hadn’t learned the term yet). Some of this section will be a rehash of work done in the beginning of chapter 2, but we’ll cover the process a lot faster this time, as well as discuss more new terminology.
注意另一个经常使用的术语是灰盒。它的意思是一样的。我倾向于使用白盒,因为这是我最先学到的术语,但其他人使用灰盒,这也是可以接受的。实际使用的颜色无论如何都有所不同,类似于蓝图不一定是蓝色。
NOTE Another term that is frequently used is grayboxing. It means the same thing. I tend to use whiteboxing because that was the term I first learned, but others use grayboxing, which is just as accepted. The actual color used varies anyway, similar to the way blueprints aren’t necessarily blue.
阻塞用空白几何体绘制场景有几个目的。首先,这个过程可以让你快速绘制草图,并随着时间的推移逐步完善。这项活动与关卡设计和/或关卡设计师密切相关。
Blocking out the scene with blank geometry serves a couple of purposes. First, this process enables you to quickly build a sketch that will be progressively refined over time. This activity is closely associated with level design and/or level designers.
定义 关卡设计是规划和创建游戏中场景(或关卡)的学科。关卡设计师是一位关卡设计的实践者。
DEFINITION Level design is the discipline of planning and creating scenes (or levels) in the game. A level designer is a practitioner of level design.
随着游戏开发团队规模的扩大和团队成员的专业化程度的提高,一种常见的关卡构建工作流程是关卡设计师通过白盒测试创建关卡的第一个版本。然后将这个粗略的关卡交给艺术团队进行视觉润色。但即使在一个很小的团队中,同一个人既设计关卡又为游戏创作艺术,这种先进行白盒测试然后润色视觉效果的工作流程通常效果最好。毕竟,你必须从某个地方开始,而白盒测试为构建视觉效果提供了明确的基础。
As game development teams have grown in size and team members have become more specialized, a common level-building workflow is for the level designer to create a first version of the level through whiteboxing. This rough level is then handed over to the art team for visual polish. But even on a tiny team, where the same person is both designing levels and creating art for the game, this workflow of first doing whiteboxing and then polishing the visuals generally works best. You have to start somewhere, after all, and whiteboxing gives a clear foundation on which to build up the visuals.
白盒测试的第二个目的是让关卡快速达到可玩状态。关卡可能尚未完成(事实上,白盒测试之后的关卡还远远未完成),但这个粗略版本是可以运行的,可以支持游戏玩法。至少,玩家可以在场景中走动(想想第 2 章中的演示)。通过这种方式,您可以在投入大量时间和精力进行详细工作之前进行测试,以确保关卡组合良好(例如,房间大小是否适合这款游戏?)。如果出现问题(比如您意识到空间需要更大),在白盒测试阶段进行更改和重新测试要容易得多。
A second purpose served by whiteboxing is that the level quickly reaches a playable state. The level may not be finished (indeed, a level right after whiteboxing is far from finished), but this rough version is functional and can support gameplay. At a minimum, the player can walk around the scene (think of the demo in chapter 2). In this way, you can test to make sure the level is coming together well (for example, are the rooms the right size for this game?) before investing a lot of time and energy in detailed work. If something is off (say you realize the spaces need to be bigger), changing and retesting is much easier in the whiteboxing stage.
此外,能够玩到正在建设中的关卡可以极大地鼓舞士气。不要低估这一好处:构建一个场景的所有视觉效果可能需要花费大量时间,而要等待很长时间才能在游戏中体验到这些工作,可能会让人觉得很辛苦。白盒化可以立即构建一个完整(虽然很原始)的关卡,然后随着游戏的不断改进,玩游戏是令人兴奋的。
Moreover, being able to play the under-construction level is a huge morale boost. Don’t discount this benefit: building all the visuals for a scene can take a great deal of time, and having to wait a long time before you can experience any of that work in the game can start to feel like a slog. Whiteboxing builds a complete (if primitive) level right away, and it’s exciting to then play the game as it continually improves.
好的,现在你明白了为什么关卡从白盒开始。现在让我们构建一个等级!
All right, so you understand why levels start with whiteboxing. Now let’s build a level!
建筑电脑上的关卡设计遵循纸上设计的关卡。我们不会深入讨论关卡设计;正如第 2 章关于游戏设计所述,关卡设计(游戏设计的一个子集)是一门庞大的学科,可以写满整本书。为了我们的目的,我们将绘制一个基本关卡,计划中几乎没有设计,以便给我们一个努力的目标。
Building a level on the computer follows designing the level on paper. We’re not going to get into a huge discussion about level design; just as chapter 2 noted about game design, level design (which is a subset of game design) is a large discipline that could fill an entire book by itself. For our purposes, we’re going to draw a basic level, with little design going into the plan, in order to give us a target to work toward.
图 4.1 是一个简单的布局的俯视图,其中四个房间由中央走廊连接。这就是我们现在需要的计划:一堆分开的区域和要放置的内墙。在真正的游戏中,你的计划会更加广泛,包括敌人和物品等。
Figure 4.1 is a top-down drawing of a simple layout with four rooms connected by a central hallway. That’s all we need for a plan right now: a bunch of separated areas and interior walls to place. In a real game, your plan would be more extensive and include things like enemies and items.
Figure 4.1 Floor plan for the level: four rooms and a central corridor
你可以通过绘制这个平面图来练习白盒法,或者你也可以绘制自己的简单楼层来练习这一步。房间布局的细节对这个练习来说并不重要。对于我们的目的来说,重要的是绘制一个平面图,这样我们就可以继续进行下一步步。
You could practice whiteboxing by building this floor plan, or you could draw your own simple level to practice that step too. The specifics of the room layout matter little for this exercise. The important thing for our purposes is to have a floor plan drawn so that we can move forward with the next step.
建筑根据绘制的平面图进行白盒关卡涉及定位和缩放一堆空白盒子作为图中的墙壁。如第 2.2.1 节所述,选择 GameObject > 3D Object > Cube 以创建一个空白盒子,您可以根据需要定位和缩放它。
Building the whitebox level in accordance with the drawn floor plan involves positioning and scaling a bunch of blank boxes to be the walls in the diagram. As described in section 2.2.1, choose GameObject > 3D Object > Cube to create a blank box that you can position and scale as needed.
第一个对象将是场景的地板。在 Inspector 中,重命名该对象并将其降低到-0.5 Y 以考虑盒子本身的高度(图 4.2 描述了这一点)。然后沿 x 轴和 z 轴拉伸该对象。
The first object will be the floor of the scene. In the Inspector, rename the object and lower it to -0.5 Y to account for the height of the box itself (figure 4.2 depicts this). Then stretch the object along the x- and z-axes.
Figure 4.2 Inspector view of the box positioned and scaled for the floor
重复这些步骤来创建场景的墙壁。您可能希望通过使墙壁成为公共基础对象的子对象来清理层次结构视图(请记住,将根对象定位在0、0、0,然后将对象拖到层次结构中) ,但这不是必需的。还要在场景周围放置一些简单的灯光,以便您可以看到它;参考第 2 章,通过在 GameObject 菜单的 Light 子菜单中选择它们来创建灯光。完成白盒化后,关卡应该看起来像图 4.3。
Repeat these steps to create the walls of the scene. You probably want to clean up the Hierarchy view by making the walls children of a common base object (remember, position the root object at 0, 0, 0, and then drag objects onto it in Hierarchy), but that’s not required. Also put a few simple lights around the scene so that you can see it; referring to chapter 2, create lights by selecting them in the Light submenu of the GameObject menu. The level should look something like figure 4.3 once you’re done with whiteboxing.
Figure 4.3 Whitebox level of the floor plan in figure 4.1
设置玩家对象或相机以移动(使用角色控制器和移动脚本创建玩家;如果需要完整说明,请参阅第 2 章)。现在,您可以在原始场景中走动,体验您的工作并进行测试。这就是您进行白盒化的方法!非常简单——但您现在只有空白的几何体,所以让我们用图片装饰几何体这墙壁。
Set up your player object or camera to move around (create the player with a character controller and movement scripts; refer to chapter 2 if you need a full explanation). Now you can walk around the primitive scene to experience your work and test it out. And that’s how you do whiteboxing! Pretty simple—but all you have right now is blank geometry, so let’s dress up the geometry with pictures on the walls.
这此时的关卡只是一张草图。它可以玩,但显然还需要在场景的视觉外观上做更多的工作。改善关卡外观的下一步是应用纹理。
The level at this point is a rough sketch. It’s playable, but clearly a lot more work needs to be done on the visual appearance of the scene. The next step in improving the look of the level is applying textures.
定义纹理是用于增强 3D 图形的 2D 图像。这就是该术语的全部含义;不要误以为纹理的任何用途都是该术语定义的一部分。无论图像如何使用,它仍然被称为纹理。
DEFINITION A texture is a 2D image being used to enhance 3D graphics. That’s the totality of what the term means; don’t confuse yourself by thinking that any of the uses of textures are part of how the term is defined. No matter how the image is being used, it’s still referred to as a texture.
注意: 纹理通常用作动词和名词。除了名词定义外,该词还描述了在 3D 图形中使用 2D 图像的操作。
NOTE Texture is routinely used as both a verb and a noun. In addition to the noun definition, the word describes the action of using 2D images in 3D graphics.
纹理在 3D 图形中有多种用途,但最直接的用途是显示在 3D 模型的表面上。本章后面,我们将讨论它如何应用于更复杂的模型,但对于我们的白盒关卡,2D 图像将充当覆盖墙壁的墙纸(见图 4.4)。
Textures have multiple uses in 3D graphics, but the most straightforward use is to be displayed on the surface of 3D models. Later in this chapter, we’ll discuss how this works for more complex models, but for our whiteboxed level, the 2D images will act as wallpaper covering the walls (see figure 4.4).
Figure 4.4 Comparing the level before and after textures
从图 4.4 的比较中可以看出,纹理将明显不真实的数字构造变成了一堵砖墙。纹理的其他用途包括用于裁剪形状的蒙版和用于使表面凹凸不平的法线贴图。稍后,您可能希望在附录 D 中提到的资源中查找有关纹理的更多信息。
As you can see from the comparison in figure 4.4, textures turn what was an obviously unreal digital construct into a brick wall. Other uses for textures include masks to cut out shapes and normal maps to make surfaces bumpy. Later, you may want to look up more information about textures in the resources mentioned in appendix D.
一个有多种文件格式可用于保存 2D 图像,那么您应该使用哪种格式?Unity 支持使用多种文件格式,因此您可以选择表 4.2 中所示的任何一种格式。
A variety of file formats is available for saving 2D images, so which should you use? Unity supports the use of many file formats, so you could choose any of the ones shown in table 4.2.
Table 4.2 2D image file formats supported by Unity
定义alpha通道用于存储图像中的透明度信息。可见颜色有三个信息通道:红色、绿色和蓝色。Alpha 是一个不可见的附加信息通道,但可控制图像的透明度。
DEFINITION The alpha channel is used to store transparency information in an image. The visible colors come in three channels of information: Red, Green, and Blue. Alpha is an additional channel of information that isn’t visible but controls the transparency of the image.
虽然 Unity 可以接受表 4.2 中所示的任何图像类型来导入和用作纹理,但文件格式在它们支持的功能方面差异很大。对于作为纹理导入的 2D 图像,有两个因素特别重要:图像是如何压缩的,以及它是否有 alpha 通道?
Although Unity will accept any of the image types shown in table 4.2 to import and use as a texture, the file formats vary considerably in the features they support. Two factors are particularly important for 2D images imported as textures: how is the image compressed, and does it have an alpha channel?
alpha 通道是一个简单的考虑因素。由于 alpha 通道经常用于 3D 图形,因此具有 alpha 通道的图像是首选。
The alpha channel is a straightforward consideration. Because the alpha channel is used often in 3D graphics, an image that has an alpha channel is preferred.
图像压缩是一个稍微复杂一点的问题,但归根结底就是“有损压缩不好”。无压缩和无损压缩都可以保持图像质量,而有损压缩会降低图像质量(因此称为有损),以减小文件大小。
Image compression is a slightly more complicated consideration, but it boils down to “lossy compression is bad.” Both no compression and lossless compression preserve the image quality, whereas lossy compression reduces the image quality (hence the term lossy) as part of reducing the file size.
考虑到以上两个因素,我建议 Unity 纹理使用 PNG 和 TGA 两种文件格式。Targas在 PNG 被广泛用于互联网之前,TGA 曾是 3D 图形纹理的首选文件格式。如今,PNG 在技术上几乎与 PNG 相当,但应用范围更广,因为它既可用于网络,又可用于纹理。
Between these two considerations, the two file formats I recommend for Unity textures are PNG and TGA. Targas (TGA) used to be the favorite file format for texturing 3D graphics, before PNG became widely used on the internet. These days, PNG is almost equivalent technologically but is much more widespread, because it’s useful both on the web and as a texture.
PSD 也常被推荐用于 Unity 纹理,因为它是一种高级文件格式,而且在 Photoshop 中处理的文件在 Unity 中也能使用,非常方便。但我倾向于将工作文件与导出到 Unity 的“完成”文件分开保存(这种想法在 3D 模型中再次出现)。
PSD is also commonly recommended for Unity textures, because it’s an advanced file format and because it’s convenient that the same file you work on in Photoshop also works in Unity. But I tend to prefer keeping work files separate from “finished” files that are exported over to Unity (this same mindset comes up again later with 3D models).
结果是,我在示例项目中提供的所有图像都是 PNG,我建议你也使用该文件格式。做出这个决定后,是时候将一些图像导入 Unity 并将它们应用到空白场景。
The upshot is that all the images I provide in the example projects are PNG, and I recommend that you work with that file format as well. With this decision made, it’s time to bring some images into Unity and apply them to the blank scene.
让我们开始创建和准备我们将要使用的纹理。用于纹理级别的图像通常是可平铺的,因此它们可以在地板等大面积表面上重复。
Let’s start creating and preparing the textures we’ll use. The images used to texture levels are usually tileable so that they can be repeated across large surfaces like the floor.
定义可平铺图像(有时称为无缝拼贴)是一种图像,当并排放置时,相对边缘会匹配。这样,图像可以重复,重复之间没有任何可见的接缝。3D 纹理的概念就像网页上的壁纸一样。
DEFINITION A tileable image (sometimes referred to as a seamless tile) is an image in which opposite edges match up when placed side by side. This way, the image can be repeated without any visible seams between the repeats. The concept for 3D texturing is just like wallpaper on web pages.
您可以通过多种方式获取可平铺图像,包括处理照片或手工绘制。这些技术的教程和说明可以在许多书籍和网站上找到,但我们现在不想陷入其中。相反,让我们从众多提供此类图像目录供 3D 艺术家使用的网站中获取几张可平铺图像。
You can obtain tileable images in several ways, including by manipulating photographs or even painting them by hand. Tutorials and explanations of these techniques can be found in numerous books and websites, but we don’t want to get bogged down with that right now. Instead, let’s grab a couple of tileable images from one of the many websites that offer a catalog of such images for 3D artists to use.
我从www.textures.com获取了几张图片(见图 4.5),用于制作关卡的墙壁和地板。找到几张你认为适合制作地板和墙壁的图片;我选择了 BrickRound0067 和 BrickLargeBare0032。
I obtained a couple of images from www.textures.com (see figure 4.5) to apply to the walls and floor of the level. Find a couple of images you think look good for the floor and walls; I chose BrickRound0067 and BrickLargeBare0032.
图 4.5 无缝平铺石头和砖块图像取自 Textures.com
Figure 4.5 Seamlessly tiling stone and brick images obtained from Textures.com
下载所需的图片并准备用作纹理。从技术上讲,您可以直接使用下载的图片,但它们并不适合用作纹理。虽然它们当然可以平铺(您使用这些图片的重要原因),但它们的尺寸不合适,文件格式也不合适。
Download the images you want and prepare them for use as textures. Technically, you could use the images directly as they were downloaded, but they aren’t ideal for use as textures. Although they’re certainly tileable (the important reason you’re using these images), they aren’t the right size and are the wrong file format.
纹理的大小(以像素为单位)应为 2 的幂。出于技术效率的原因,图形芯片喜欢处理大小为 2 N 的纹理:4、8、16、32、64、128、256、512、1024、2048(下一个数字是 4096,但此时图像太大,无法用作纹理)。在图像编辑器(Photoshop、GIMP 或其他;请参阅附录 B)中,将下载的图像缩放为 256 × 256 像素,并将其保存为 PNG。
The size (in pixels) of a texture should be in powers of 2. For reasons of technical efficiency, graphics chips like to handle textures in sizes that are 2N: 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048 (the next number is 4096, but at that point the image is too big to use as a texture). In your image editor (Photoshop, GIMP, or whatever; refer to appendix B), scale the downloaded image to 256 × 256 pixels, and save it as a PNG.
现在将文件从计算机中的位置拖到 Unity 中的“项目”视图中。这会将文件复制到您的 Unity 项目中(见图 4.6),此时它们将作为纹理导入并可用于 3D 场景。如果拖动文件不方便,您可以右键单击“项目”并选择“导入新资产”以访问文件选择器。
Now drag the files from their location in the computer into the Project view in Unity. This will copy the files into your Unity project (see figure 4.6), at which point they’re imported as textures and can be used in the 3D scene. If dragging the file over would be awkward, you could instead right-click in Project and select Import New Asset to access a file picker.
图 4.6 从 Unity 外部拖拽图像导入到 Project 视图中。
Figure 4.6 Drag images from outside Unity to import them into the Project view.
提示:随着项目变得越来越复杂,将资源组织到单独的文件夹中可能是一个好主意。在“项目”视图中,为“脚本”和“纹理”创建文件夹,然后将资源移动到相应的文件夹中。只需将文件拖到新文件夹中即可。
TIP Organizing your assets into separate folders is probably a good idea as your projects start to get more complex. In the Project view, create folders for Scripts and Textures and then move assets into the appropriate folders. Simply drag files to their new folder.
警告Unity 有几个关键字,它会在文件夹名称中做出响应,并以特殊方式处理这些特殊文件夹的内容。这些关键字是 Resources、Plugins、Editor 和 Gizmos。在本书的后面,我们将介绍其中一些特殊文件夹的作用,但目前,请避免使用这些词命名任何文件夹。
WARNING Unity has several keywords that it responds to in folder names, with special ways of handling the contents of these special folders. Those keywords are Resources, Plugins, Editor, and Gizmos. Later in the book, we’ll go over what some of these special folders do, but for now, avoid naming any folders with those words.
现在图像已作为纹理导入 Unity,可供使用。但我们如何将纹理应用到场景?
Now the images are imported into Unity as textures, ready to use. But how do we apply the textures to objects in the scene?
从技术上来说,纹理不会直接应用于几何体。相反,纹理可以是材质的一部分,而材质可以应用于几何体。如简介中所述,材质是定义表面属性的一组信息;该信息可以包括要在该表面上显示的纹理。这种间接性很重要,因为同一纹理可以与多种材质一起使用。也就是说,通常每个纹理都与不同的材质搭配使用,因此为了方便起见,Unity 允许您将纹理放到对象上,然后它会自动创建新材质。
Technically, textures aren’t applied to geometry directly. Instead, textures can be part of materials, and materials are applied to geometry. As explained in the introduction, a material is a set of information defining the properties of a surface; that information can include a texture to display on that surface. This indirection is significant because the same texture can be used with multiple materials. That said, typically each texture goes with a different material, so for convenience Unity allows you to drop a texture onto an object and then it creates a new material automatically.
如果您将纹理从“项目”视图拖到场景中的对象上,Unity 将创建新材质并将其应用于该对象。图 4.7 说明了该操作。现在尝试使用地板纹理。
If you drag a texture from Project view onto an object in the scene, Unity will create a new material and apply it to the object. Figure 4.7 illustrates the maneuver. Try that now with the texture for the floor.
图 4.7 应用纹理的一种方法是将它们从项目拖到场景对象上。
Figure 4.7 One way to apply textures is to drag them from Project onto Scene objects.
除了这种自动创建材质的便捷方法之外,创建材质的“正确”方法是选择 Assets > Create > Material;新资产将出现在 Project 视图中。现在选择材质以在 Inspector 中显示其属性(您将看到类似图 4.8 的内容),然后将纹理拖到主纹理槽中;该设置称为 Albedo(这是基色的技术术语),纹理槽是标签左侧的方块。同时,将材质从 Project 拖到场景中的对象上,以将材质应用于该对象。现在使用墙壁纹理尝试以下步骤:创建新材质,将墙壁纹理拖到此材质中,然后将材质拖到场景中的墙壁上。
Besides this convenient method of automatically creating materials, the “proper” way to create a material is to choose Assets > Create > Material; the new asset will appear in the Project view. Now select the material to show its properties in the Inspector (you’ll see something like figure 4.8) and drag a texture to the main texture slot; the setting is called Albedo (that’s a technical term for the base color), and the texture slot is the square to the left side of the label. Meanwhile, drag the material up from Project onto an object in the scene to apply the material to that object. Try these steps now with the texture for the wall: create a new material, drag the wall texture into this material, and drag the material onto a wall in the scene.
图 4.8 选择一种材质在检查器中查看它,然后将纹理拖到材质属性中。
Figure 4.8 Select a material to see it in the Inspector and then drag textures to the material properties.
现在您应该看到石头和砖块的图像出现在地板和墙壁物体的表面上,但图像看起来相当拉伸和模糊。单个图像被拉伸以覆盖整个地板。相反,您希望图像在地板表面上重复几次。
You should now see the stone and brick images appear on the surface of the floor and wall objects, but the images look rather stretched out and blurry. The single image is being stretched out to cover the entire floor. Instead, you want the image to repeat a few times over the floor surface.
您可以使用材质的平铺属性来设置此外观。在项目中选择材质,然后在检查器中更改平铺数(每个方向的平铺都有单独的 X 和 Y 值)。确保您设置的是主贴图的平铺,而不是次要贴图的平铺(此材质可选择使用次要纹理贴图来实现高级效果)。默认平铺为1(即无平铺,图像被拉伸到整个表面);将数字更改为8之类的数字,看看场景中会发生什么。将两种材质中的数字更改为看起来不错的平铺。
You can set this appearance by using the tiling property of the material. Select the material in Project and then change the tiling number in the Inspector (with separate X and Y values for tiling in each direction). Make sure you’re setting the tiling of the main map and not the secondary map (this material optionally uses a secondary texture map for advanced effects). The default tiling is 1 (that’s no tiling, with the image being stretched over the entire surface); change the numbers to something like 8 and see what happens in the scene. Change the numbers in both materials to tiling that looks good.
注意:像这样调整平铺属性仅适用于纹理白盒几何体。在精致的游戏中,地板和墙壁将使用更复杂的艺术工具构建,其中包括设置它们的纹理。
NOTE Adjusting the tiling property like this is useful only for texturing whitebox geometry. In a polished game, the floor and walls will be built with more intricate art tools, and that includes setting up their textures.
太棒了——现在场景中的地板和墙壁上都应用了纹理!您还可以将纹理应用于场景中的天空。让我们看看那过程。
Great—now the scene has textures applied to the floor and walls! You can also apply textures to the sky of the scene. Let’s look at that process.
这砖块和石头纹理为墙壁和地板提供了更自然的外观。然而,天空目前是空白的,不自然的;我们也希望天空看起来更逼真。完成这项任务最常见的方法是使用天空图片进行一种特殊的纹理处理。
The brick and stone textures provide a much more natural look to the walls and floor. Yet the sky is currently blank and unnatural; we also want a realistic look for the sky. The most common approach to this task is a special kind of texturing using pictures of the sky.
经过默认情况下,相机的背景颜色为深蓝色。通常,该颜色会填充视图的任何空白区域(例如,此场景的墙壁上方),但可以将天空的图片渲染为背景。这就是天空盒的作用所在。
By default, the camera’s background color is dark blue. Ordinarily, that color fills in any empty area of the view (for example, above the walls of this scene), but it’s possible to render pictures of the sky as background. This is where a skybox comes in.
定义天空盒是一个围绕着摄像机的立方体,每个侧面都有天空的图片。无论摄像机朝向哪个方向,它看到的都是天空的图片。
DEFINITION A skybox is a cube surrounding the camera with pictures of the sky on each side. No matter what direction the camera is facing, it’s looking at a picture of the sky.
正确实现天空盒可能很棘手;图 4.9 显示了天空盒的工作原理图。需要使用渲染技巧才能使天空盒显示为远处的背景。幸运的是,Unity 已经为您处理了所有这些问题。
Properly implementing a skybox can be tricky; figure 4.9 shows a diagram of how a skybox works. Rendering tricks are needed for the skybox to appear as a distant background. Fortunately, Unity already takes care of all that for you.
Figure 4.9 Diagram of a skybox
新场景附带已分配的简单默认天空盒。这就是为什么天空具有从浅蓝到深蓝的渐变,而不是平坦的深蓝色。打开照明窗口(窗口 > 渲染 > 照明),切换到环境选项卡,然后注意第一个设置是天空盒材质。此窗口有多个与 Unity 中的高级照明系统相关的设置面板,但目前我们只关心第一个设置。
New scenes come with a simple default skybox already assigned to them. This is why the sky has a gradient from light to dark blue, rather than being a flat dark blue. Open the lighting window (Window > Rendering > Lighting), switch to the Environment tab, and then note that the first setting is Skybox Material. This window has a multiple settings panels related to the advanced lighting system in Unity, but for now, we care about only the first setting.
就像之前的砖块纹理一样,天空盒图像可以从各种网站获取。搜索天空盒纹理或直接从本书的示例项目中获取。例如,我从 Heiko Irrgang ( https://93i.de/ ) 获得了 TropicalSunnyDay 天空盒图像集。将此天空盒应用到场景后,您将看到类似图 4.10 的内容。
Just like the brick textures earlier, skybox images can be obtained from a variety of websites. Search for skybox textures or simply get them from the book’s sample project. For example, I obtained the TropicalSunnyDay set of skybox images from Heiko Irrgang at https://93i.de/. Once this skybox is applied to the scene, you will see something like figure 4.10.
Figure 4.10 Scene with background pictures of the sky
与其他纹理一样,天空盒图像首先被分配给材质,然后在场景中使用。让我们来看看如何创建一个新的天空盒材料。
As with other textures, skybox images are first assigned to a material, and that gets used in the scene. Let’s examine how to create a new skybox material.
第一的,创建一个新材质(像往常一样,右键单击并选择“创建”,或从“资源”菜单中选择“创建”),然后选择该材质以在 Inspector 中查看其设置。接下来,您需要更改此材质使用的着色器。材质设置的顶部有一个着色器菜单(见图 4.11)。在第 4.3 节中,我们几乎忽略了这个菜单,因为默认菜单适用于大多数标准纹理,但天空盒需要特殊的着色器。
First, create a new material (as usual, either right-click and choose Create, or choose Create from the Assets menu) and then select that material to see its settings in the Inspector. Next, you need to change the shader used by this material. The top of the material settings has a Shader menu (see figure 4.11). In section 4.3, we pretty much ignored this menu because the default works fine for most standard texturing, but a skybox requires a special shader.
Figure 4.11 The drop-down menu of available shaders
定义着色器是一个简短的程序,概述了绘制表面的指令,包括是否使用任何纹理。计算机使用这些指令来计算渲染图像时的像素。最常见的着色器采用材质的颜色并根据光线使其变暗,但着色器也可用于各种视觉效果。
DEFINITION A shader is a short program that outlines instructions for drawing a surface, including whether to use any textures. The computer uses these instructions to calculate the pixels when rendering the image. The most common shader takes the color of the material and darkens it according to the light, but shaders can also be used for all sorts of visual effects.
每种材质都有一个控制它的着色器(您可以将材质视为着色器的一个实例)。新材质默认设置为标准着色器。此着色器显示材质的颜色(包括纹理),同时在表面上应用光线和阴影。
Every material has a shader that controls it (you could think of a material as an instance of a shader). New materials are set to the Standard shader by default. This shader displays the color of the material (including the texture) while applying light and shadows across the surface.
对于天空盒,Unity 有一个不同的着色器。单击菜单可查看所有可用着色器的下拉列表(见图 4.11)。选择“天空盒”部分,然后在子菜单中选择“6 Sided”。激活此着色器后,材质现在有六个大纹理槽(而不是标准着色器仅有的一个小 Albedo 纹理槽)。这六个纹理槽对应于立方体的六个面,因此这些图像应在边缘处匹配以显得无缝。例如,图 4.12 显示了阳光明媚的天空盒的图像。
For skyboxes, Unity has a different shader. Click the menu to see the drop-down list (see figure 4.11) of all the available shaders. Select the Skybox section and choose 6 Sided in the submenu. With this shader active, the material now has six large texture slots (instead of only the small Albedo texture slot that the standard shader has). These six texture slots correspond to the six sides of a cube, so these images should match up at the edges to appear seamless. For example, figure 4.12 shows the images for the sunny skybox.
Figure 4.12 Six images for the sides of a skybox
将天空盒图像导入 Unity 的方式与导入砖块纹理的方式相同:将文件拖到 Project 视图中,或在 Project 中单击鼠标右键,然后选择“Import New Asset”。我们需要更改一个细微的导入设置;单击导入的纹理以在 Inspector 中查看其属性,并将 Wrap Mode 设置(如图 4.13 所示)从 Repeat 更改为 Clamp。完成后,别忘了单击 Apply。通常,纹理可以重复平铺在表面上,为了使其看起来无缝,图像的相对边缘会融合在一起。但是这种边缘混合会在图像相交的天空中产生模糊的线条,因此 Clamp 设置(类似于第 2 章中的Clamp()函数)将限制纹理的边界并消除这种混合。
Import the skybox images into Unity the same way you brought in the brick textures: drag the files into the Project view or right-click in Project and select Import New Asset. We need to change one subtle import setting; click the imported texture to see its properties in the Inspector and change the Wrap Mode setting (shown in figure 4.13) from Repeat to Clamp. Don’t forget to click Apply when you’re done. Ordinarily, textures can be tiled repeatedly over a surface, and for this to appear seamless, opposite edges of the image bleed together. But this blending of edges can create faint lines in the sky where images meet, so the Clamp setting (similar to the Clamp() function in chapter 2) will limit the boundaries of the texture and get rid of this blending.
Figure 4.13 Correct faint edge lines by adjusting the Wrap mode.
现在,您可以将这些图像拖到天空盒材质的纹理槽中。图像名称与要分配它们的纹理槽相对应(例如左侧或正面)。一旦所有六个纹理都链接起来,您就可以使用这个新材质作为场景的天空盒。再次打开照明窗口并将这个新材质设置为天空盒槽;将材质拖到该槽,或单击小圆圈图标以调出文件选择器。现在单击“播放”以查看新的天空盒。
Now you can drag these images to the texture slots of the skybox material. The image names correspond to the texture slot to assign them to (such as left or front). Once all six textures are linked up, you can use this new material as the skybox for the scene. Open the lighting window again and set this new material to the Skybox slot; either drag the material to that slot, or click the tiny circle icon to bring up a file picker. Now click Play to see the new skybox.
提示默认情况下,Unity 将在编辑器的“场景”视图中显示天空盒(或至少显示其主色)。您可能会发现此颜色在编辑对象时会分散注意力,因此您可以打开或关闭天空盒。场景视图窗格顶部是控制可见内容的按钮;找到“效果”按钮以打开或关闭天空盒。
TIP By default, Unity will display the skybox (or at least its main color) in the editor’s Scene view. You may find this color distracting while editing objects, so you can toggle the skybox on or off. Across the top of the Scene view’s pane are buttons that control what’s visible; look for the Effects button to toggle the skybox on or off.
哇哦——您已经学会了如何为场景创建天空视觉效果!天空盒是一种优雅的方式,可以营造出玩家周围广阔氛围的幻觉。完善关卡视觉效果的下一步是创建更复杂的3D模型。
Woo-hoo—you’ve learned how to create sky visuals for your scene! A skybox is an elegant way to create the illusion of a vast atmosphere surrounding the player. The next step in polishing the visuals in your level is to create more complex 3D models.
在在前面的部分中,我们研究了如何将纹理应用于关卡的大面积平坦墙壁和地板。但是更详细的物体呢?比如说,如果你想要在房间里放置有趣的家具怎么办?您可以通过在外部 3D 艺术应用程序中构建 3D 模型来实现这一点。回想一下本章简介中的定义:3D 模型是游戏中的网格对象(三维形状)。好吧,您将导入一个简单长凳的 3D 网格。
In the previous sections, we looked at applying textures to the large flat walls and floors of the level. But what about more detailed objects? What if you want, say, interesting furniture in the room? You can accomplish that by building 3D models in external 3D art apps. Recall the definition from the introduction to this chapter: 3D models are the mesh objects in the game (the three-dimensional shapes). Well, you’re going to import a 3D mesh of a simple bench.
广泛用于建模 3D 对象的应用程序包括 Autodesk Maya 和 Autodesk 3ds Max。两者都是昂贵的商业工具,因此本章的示例使用开源应用程序 Blender。示例下载包含一个您可以使用的 .blend 文件;图 4.14 描绘了 Blender 中的长凳模型。如果您有兴趣学习如何建模自己的对象,您可以在附录 C 中找到有关在 Blender 中建模此长凳的练习。
Applications widely used for modeling 3D objects include Autodesk Maya and Autodesk 3ds Max. Both are expensive commercial tools, so the sample for this chapter uses the open source app Blender. The sample download includes a .blend file that you can use; figure 4.14 depicts the bench model in Blender. If you’re interested in learning how to model your own objects, you’ll find an exercise in appendix C about modeling this bench in Blender.
Figure 4.14 The bench model in Blender
除了您自己或与您合作的艺术家创建的定制模型外,还可以从游戏艺术网站下载许多 3D 模型。Unity Asset Store 是 3D 模型的绝佳资源,可在 Unity 内或https://assetstore.unity.com上访问。
Besides custom-made models created by yourself or an artist you’re working with, many 3D models are available for download from game art websites. One great resource for 3D models is the Unity Asset Store, accessible within Unity or at https://assetstore.unity.com.
后如果您获得了使用外部艺术工具制作的模型,则需要从该软件导出资产。与 2D 图像一样,导出 3D 模型时可以使用多种文件格式,这些文件类型各有优缺点。表 4.3 列出了 Unity 支持的 3D 文件格式。
After you obtain a model made in an external art tool, you need to export the asset from that software. Just as with 2D images, multiple file formats are available for you to use when exporting the 3D model, and these file types have various pros and cons. Table 4.3 lists the 3D file formats that Unity supports.
Table 4.3 3D Model file formats supported by Unity
|
Mesh and animation; another good option when FBX isn’t available. |
|
|
仅限网格;这是一种文本格式,因此有时对于通过互联网进行流式传输很有用。 Mesh only; this is a text format, so sometimes useful for streaming over the internet. |
|
选择哪个选项归根结底取决于文件是否支持动画。由于 COLLADA 和 FBX 是仅有的两个包含动画数据的选项,因此这两个选项是可选的。只要可用(并非所有 3D 工具都将其作为导出选项),FBX 导出往往效果最好,但如果您使用的工具没有 FBX 导出,那么 COLLLADA 也能很好地工作。在我们的例子中,Blender 支持 FBX 导出,因此我们将使用该文件格式。
Choosing an option boils down to whether the file supports animation. Because COLLADA and FBX are the only two options that include animation data, those are the two options to choose. Whenever it’s available (not all 3D tools have it as an export option), FBX export tends to work best, but if you’re using a tool without FBX export, then COLLLADA works well too. In our case, Blender supports FBX export, so we’ll use that file format.
请注意,表 4.3 底部列出了几个 3D 艺术应用程序。Unity 允许您将这些应用程序的文件直接放入您的项目中。此功能乍一看很方便,但有一些注意事项。
Note that the bottom of table 4.3 lists several 3D art applications. Unity allows you to directly drop those applications’ files into your project. This functionality seems handy at first but has some caveats.
首先,Unity 不会直接加载这些应用程序文件;相反,它会在后台导出模型并加载导出的文件。因为模型无论如何都会导出到 FBX,所以最好明确执行该步骤。此外,此导出要求您安装相关应用程序。如果您计划在多台计算机之间共享文件(例如,一组开发人员一起工作),则此要求会很麻烦。我不建议直接在统一。
For starters, Unity doesn’t load those application files directly; instead, it exports the model behind the scenes and loads that exported file. Because the model is being exported to FBX anyway, it’s preferable to do that step explicitly. Furthermore, this export requires you to have the relevant application installed. This requirement is a big hassle if you plan to share files among multiple computers (for example, a team of developers working together). I don’t recommend using 3D art application files directly in Unity.
全部好了,现在是时候从 Blender 导出模型,然后将其导入 Unity 了。首先,在 Blender 中打开工作台,然后选择文件 > 导出 > FBX。保存文件后,以与导入图像相同的方式将其导入 Unity。将 FBX 文件从计算机拖到 Unity 的项目视图中,或在项目中右键单击并选择导入新资产。3D 模型将被复制到 Unity 项目中,并显示为可放入场景中。
All right, it’s time to export the model from Blender and then import it into Unity. First, open the bench in Blender and then choose File > Export > FBX. Once the file is saved, import it into Unity the same way that you import images. Drag the FBX file from the computer into Unity’s Project view or right-click in Project and choose Import New Asset. The 3D model will be copied into the Unity project and show up ready to be put in the scene.
注意:示例下载包含 .blend 文件,以便您可以练习从 Blender 导出 FBX 文件。即使您最终没有自己建模,也可能需要将下载的模型转换为 Unity 接受的格式。如果您想跳过涉及 Blender 的所有步骤,请使用提供的 FBX 文件。
NOTE The sample download includes the .blend file so that you can practice exporting the FBX file from Blender. Even if you don’t end up modeling anything yourself, you may need to convert downloaded models into a format Unity accepts. If you want to skip all steps involving Blender, use the provided FBX file.
您应该立即更改一些导入设置。首先,Unity 默认导入的模型比例非常小(参见图 4.15,其中显示了选择模型时在 Inspector 中看到的内容);将比例因子更改为 50,以部分抵消 0.01 单位转换。您可能还想单击“生成碰撞器”复选框,但这是可选的;如果没有碰撞器,您可以穿过长凳。然后,切换到导入设置中的“动画”选项卡并取消选择“导入动画”(此模型没有动画)。进行这些更改后,单击底部的应用。
You should change a few import settings immediately. First, Unity defaults imported models to a very small scale (refer to figure 4.15, which shows what you see in the Inspector when you select the model); change the Scale Factor to 50 to partially counteract the 0.01 unit conversion. You may also want to click the Generate Colliders check box, but that’s optional; without a collider, you can walk through the bench. Then, switch to the Animation tab in the import settings and deselect Import Animation (this model doesn’t have animation). Click Apply at the bottom after making these changes.
Figure 4.15 Adjust import settings for the 3D model.
这样就搞定了导入的网格。现在来处理纹理。导入长凳纹理(图 4.16 中的图像)的方式与之前导入墙壁砖块的方式相同:将图像文件从此项目的临时文件夹拖到 Unity 的项目视图中,或在项目中右键单击并选择导入新资产。图像看起来有点奇怪,图像的不同部分出现在长凳的不同部分;模型的纹理坐标经过编辑,以定义图像到网格的映射。
That takes care of the imported mesh. Now for the texture. Import the bench texture (the image in figure 4.16) in the same way as the bricks for walls earlier: drag the image file from this project’s scratch folder into Unity’s Project view, or right-click in Project and select Import New Asset. The image looks somewhat odd, with different parts of the image appearing on different parts of the bench; the model’s texture coordinates were edited to define this mapping of image to mesh.
Figure 4.16 The 2D image for the bench texture
定义 纹理坐标是每个顶点的一组额外值,用于将多边形分配给纹理图像的区域。想象一下包装纸;3D 模型是被包装的盒子,纹理是包装纸,纹理坐标表示包装纸将放在盒子上的点。
DEFINITION Texture coordinates are an extra set of values for each vertex that assign polygons to areas of the texture image. Think about it like wrapping paper; the 3D model is the box being wrapped, the texture is the wrapping paper, and the texture coordinates represent the points on the box where the wrapping paper will go.
注意,即使您不想对长凳进行建模,您可能也想阅读附录 C 中有关纹理坐标的详细解释。理解纹理坐标(以及其他相关术语,如UV和映射)在编写游戏程序时很有用。
NOTE Even if you don’t want to model the bench, you may want to read the detailed explanation of texture coordinates in appendix C. Texture coordinates (as well as other related terms like UVs and mapping) can be useful to understand when programming games.
当 Unity 导入 FBX 文件时,它还会生成一个与 Blender 中的材质设置相同的材质。如果 Blender 中使用的图像文件已导入 Unity,则生成的材质将自动链接到该纹理。如果自动链接无法正常工作,或者您需要使用不同的纹理图像,则可以提取模型的材质以进行进一步编辑。请回头参考图 4.15;在“材质”选项卡下,您应该会找到一个标有“提取材质”的按钮。现在您可以选择材质资产,然后将图像拖到 Albedo,就像您对砖墙所做的那样。
When Unity imported the FBX file, it also generated a material with the same settings as the material in Blender. If the image file used in Blender has been imported into Unity, the generated material will automatically link to that texture. If the automatic linkage doesn’t work right, or if you need to use a different texture image, then you can extract the model’s material for further editing. Refer back to figure 4.15; under the Materials tab, you should find a button labeled Extract Materials. Now you can select the material asset and then drag images to Albedo just as you did for brick walls.
新材料通常太亮,因此您可能需要将“平滑度”设置降低到0(表面越光滑,光泽度越高)。最后,根据需要调整好一切后,您可以将长凳放入场景中。将模型从“项目”视图向上拖动,并将其放置在关卡的一个房间中;拖动鼠标时,您应该会在场景中看到它。将长凳放到位后,您应该会看到类似图 4.17 的内容。恭喜您 - 您已经为创建了一个纹理模型这等级!
New materials are often too shiny, so you may want to reduce the Smoothness setting to 0 (smoother surfaces are shinier). Finally, having adjusted everything as needed, you can put the bench in the scene. Drag the model up from the Project view and place it in one room of the level; as you drag the mouse, you should see it in the scene. Once you drop the bench in place, you should see something like figure 4.17. Congratulations—you’ve created a textured model for the level!
注意我们不会在本章中这样做,但通常情况下,您还会用外部工具创建的模型替换白盒几何体。新的几何体可能看起来基本相同,但您在控制纹理方面将拥有更大的灵活性。
NOTE We’re not going to do it in this chapter, but typically, you’d also replace the whitebox geometry with models created in an external tool. The new geometry might look essentially identical, but you’ll have much more flexibility in controlling the texture.
Figure 4.17 The imported bench in the level
除了除了 2D 图像和 3D 模型之外,游戏艺术家创建的剩余视觉内容类型是粒子系统。本章介绍中的定义解释了粒子系统是创建和控制大量移动物体的有序机制。粒子系统可用于创建火焰、烟雾或喷水等视觉效果。图 4.18 中的火焰效果是使用粒子系统创建的。
Besides 2D images and 3D models, the remaining type of visual content that game artists create is a particle system. The definition in this chapter’s introduction explained that particle systems are orderly mechanisms for creating and controlling large numbers of moving objects. Particle systems are useful for creating visual effects like fire, smoke, or spraying water. The fire effect in figure 4.18 was created using a particle system.
Figure 4.18 Fire effect created using a particle system
虽然大多数其他艺术资产都是在外部工具中创建并导入到项目中,但粒子系统是在 Unity 内部创建的。Unity 提供了灵活而强大的工具来创建粒子效果。
Whereas most other art assets are created in external tools and imported into the project, particle systems are created within Unity itself. Unity provides flexible and powerful tools for creating particle effects.
注意与 Mecanim 动画系统的情况非常相似,Unity 曾经有一个较旧的旧粒子系统,并为其新系统赋予了一个特殊名称 Shuriken。目前,旧粒子系统已逐步淘汰,因此不再需要单独的名称。
NOTE Much like the situation with the Mecanim animation system, Unity used to have an older legacy particle system and gave its newer system a special name, Shuriken. At this point, the legacy particle system has been phased out, so the separate name is no longer necessary.
首先,创建一个新的粒子系统并观察默认效果的播放。从 GameObject 菜单中,选择 Effects > Particle System,您将看到基本的白色绒球从新对象向上喷射。或者,当您选择对象时,您会看到粒子向上喷射。当您选择粒子系统时,粒子播放面板将显示在屏幕的一角,并指示已过去的时间量(见图 4.19)。
To begin, create a new particle system and watch the default effect play. From the GameObject menu, choose Effects > Particle System, and you’ll see basic white puffballs spraying upward from the new object. Or rather, you’ll see particles spraying upward while you have the object selected. When you select a particle system, the particle playback panel is displayed in the corner of the screen and indicates the amount of time that has elapsed (see figure 4.19).
Figure 4.19 Playback panel for a particle system
默认效果看起来已经很不错了,但是让我们来看看可以用来自定义效果的参数。
The default effect looks pretty neat already, but let’s go through parameters you can use to customize the effect.
图 4.20显示粒子系统的完整设置列表。我们不会逐一介绍该列表中的每个设置;相反,我们将介绍与制作火焰效果相关的设置。一旦您了解了一些设置的工作原理,其余的设置就应该相当不言自明了。每个设置的标签实际上是一个完整的信息面板。最初,只有第一个信息面板是展开的;其余面板是折叠的。单击设置的标签以展开该信息面板。
Figure 4.20 shows the entire list of settings for a particle system. We’re not going to go through every single setting in that list; instead, we’ll look at those relevant to making the fire effect. Once you understand how a few of the settings work, the rest should be fairly self-explanatory. Each setting’s label is, in fact, a whole information panel. Initially, only the first information panel is expanded; the rest of the panels are collapsed. Click the setting’s label to expand that information panel.
提示许多设置由检查器底部显示的曲线控制。该曲线表示值随时间的变化方式:图表左侧表示粒子首次出现的时间,右侧表示粒子消失的时间,底部表示值为 0 ,顶部表示最大值。拖动图表周围的点,然后双击或右键单击曲线以插入新点。
TIP Many of the settings are controlled by a curve displayed at the bottom of the Inspector. That curve represents how the value changes over time: the left side of the graph indicates when the particle first appears, the right side indicates when the particle is gone, the bottom is a value of 0, and the top is the maximum value. Drag points around the graph and double-click or right-click the curve to insert new points.
Adjust parameters of the particle system as indicated in figure 4.20, and it’ll look more like a jet of flame.
图 4.20 检查器显示粒子系统的设置(指出火焰效果的设置)。
Figure 4.20 The Inspector displays settings for a particle system (pointing out settings for the fire effect).
现在粒子系统看起来更像一股火焰,但效果仍然需要粒子看起来像火焰,而不是白色斑点。这需要将新图像导入 Unity。图 4.21 描绘了我绘制的图像;我制作了一个橙色点,并使用涂抹工具画出火焰的卷须(然后我用黄色画了同样的东西)。
Now the particle system looks more like a jet of flame, but the effect still needs the particles to look like flame, not white blobs. That requires importing a new image into Unity. Figure 4.21 depicts the image I painted; I made an orange dot and used the Smudge tool to draw out the tendrils of the flame (and then I drew the same thing in yellow).
Figure 4.21 The image used for fire particles
无论您是使用示例项目中的图像、自己绘制图像还是下载类似图像,都需要将图像文件导入 Unity。如前所述,将图像文件拖到“项目”视图中,或选择“资产”>“导入新资产”。
Whether you use this image from the sample project, draw your own, or download a similar one, you need to import the image file into Unity. As explained previously, drag image files into the Project view, or choose Assets > Import New Asset.
与 3D 模型一样,纹理不会直接应用于粒子系统。您可以将纹理添加到材质并将该材质应用于粒子系统。创建一个新材质,然后选择它以在 Inspector 中查看其属性。将火焰图像从 Project 拖到纹理槽。这将火焰纹理链接到火焰材质,因此现在您要将材质应用于粒子系统。图 4.22 显示了如何执行此操作:选择粒子系统,展开设置底部的 Renderer,然后将材质拖到 Material 槽上。
Just as with 3D models, textures aren’t applied to particle systems directly. You add the texture to a material and apply that material to the particle system. Create a new material and then select it to see its properties in the Inspector. Drag the fire image from Project up to the texture slot. That links the fire texture to the fire material, so now you want to apply the material to the particle system. Figure 4.22 shows how to do this: select the particle system, expand Renderer at the bottom of the settings, and drag the material onto the Material slot.
Figure 4.22 Assign a material to the particle system.
就像您对天空盒材质所做的那样,您需要更改粒子材质的着色器。单击材质设置顶部附近的着色器菜单以查看可用着色器列表。粒子材质需要“粒子”子菜单下的一个着色器,而不是标准默认值。如图 4.23 所示,在本例中,我们需要“标准无光”。现在将材质的渲染模式切换为“加法”。这将使粒子看起来朦胧并使场景变亮,就像一团火。
As you did for the skybox material, you need to change the shader for a particle material. Click the Shader menu near the top of the material settings to see the list of available shaders. Instead of the standard default, a material for particles needs one of the shaders under the Particles submenu. As shown in figure 4.23, in this case, we want Standard Unlit. Now switch the material’s Rendering Mode to Additive. This will make the particles appear to be hazy and brighten the scene, just like a fire.
Figure 4.23 Setting the shader for the fire particle material
定义 添加剂是一种着色器效果,它将粒子的颜色添加到其背后的颜色中,而不是替换像素。这会使像素更亮,并使粒子上的黑色变得不可见。这些着色器具有与 Photoshop 中的添加层效果相同的视觉效果。
DEFINITION Additive is a shader effect that adds the color of the particle to the color behind it, as opposed to replacing the pixels. This makes the pixels brighter and makes black on the particle turn invisible. These shaders have the same visual effect as the Additive layer effect in Photoshop.
警告更改此着色器可能会导致 Unity 发出需要应用于系统的警告。单击检查器底部的“应用于系统”按钮。
WARNING Changing this shader may cause Unity to emit a warning about needing to apply to systems. Click the Apply to Systems button at the bottom of the Inspector.
将火焰材质分配给火焰粒子效果后,现在看起来如图 4.18 所示。这看起来像一股相当逼真的火焰喷射,但这种效果仅在静止不动时才有效。接下来,让我们将它附加到一个移动的物体上大约。
With the fire material assigned to the fire particle effect, it’ll now look like figure 4.18. This looks like a pretty convincing jet of flame, but the effect doesn’t work only when sitting still. Next, let’s attach it to an object that moves around.
创造一个球体(记住,GameObject > 3D Object > Sphere)。创建一个名为BackAndForth的新脚本并将其附加到新球体。
Create a sphere (remember, GameObject > 3D Object > Sphere). Create a new script called BackAndForth and attach it to the new sphere.
Listing 4.1 Moving an object back and forth along a straight path
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 BackAndForth : MonoBehaviour {
公共浮动速度=3.0f;
公共浮动maxZ = 16.0f; ❶
公共浮点数minZ = -16.0f;
私有 int 方向 = 1; ❷
无效更新(){
transform.Translate(0, 0, 方向 * 速度 * 时间.deltaTime);
bool 反弹 = false;
如果 (transform.position.z > maxZ || transform.position.z < minZ) {
方向 = -方向; ❸
反弹=真;
}
if (反弹) { ❹
transform.Translate(0, 0, 方向 * 速度 * 时间.deltaTime);
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BackAndForth : MonoBehaviour {
public float speed = 3.0f;
public float maxZ = 16.0f; ❶
public float minZ = -16.0f;
private int direction = 1; ❷
void Update() {
transform.Translate(0, 0, direction * speed * Time.deltaTime);
bool bounced = false;
if (transform.position.z > maxZ || transform.position.z < minZ) {
direction = -direction; ❸
bounced = true;
}
if (bounced) { ❹
transform.Translate(0, 0, direction * speed * Time.deltaTime);
}
}
}
❶ These are the positions the object moves between.
❷ Which direction is the object currently moving in?
❸ Toggle the direction back and forth.
❹ Apply a second movement in the new direction if the object switched directions.
运行此脚本,球体会在关卡的中央走廊中来回滑动。现在您可以将粒子系统设为球体的子项,火焰将随球体移动。与关卡的墙壁一样,在层次结构视图中,将粒子对象拖到球体对象上。
Run this script, and the sphere glides back and forth in the central corridor of the level. Now you can make the particle system a child of the sphere, and the fire will move with the sphere. Just as with the walls of the level, in the Hierarchy view, drag the particle object onto the sphere object.
警告:通常,在将对象设为另一个对象的子对象后,必须重置其位置。例如,我们希望粒子系统位于0、0、0(相对于父对象)。Unity 将保留对象在被链接为子对象之前的位置。
WARNING You usually have to reset the position of an object after making it the child of another object. For example, we want the particle system at 0, 0, 0 (this is relative to the parent). Unity will preserve the placement of an object from before it was linked as a child.
现在粒子系统会随着球体移动。但是,火焰并没有因为移动而偏离,这看起来不自然。这是因为默认情况下,粒子只能在粒子系统的局部空间中正确移动。要完成燃烧的球体,请在粒子系统设置中找到模拟空间(位于图 4.20 的顶部面板中),然后从局部切换到世界。
Now the particle system moves along with the sphere. However, the fire isn’t deflecting from the movement, which looks unnatural. That’s because, by default, particles move correctly only in the local space of the particle system. To complete the flaming sphere, find Simulation Space in the particle system settings (it’s in the top panel of figure 4.20) and switch from Local to World.
注意:在此脚本中,对象沿直线来回移动,但视频游戏中的对象通常沿复杂路径移动。 Unity 支持复杂的导航和路径;请参阅https://docs.unity3d.com/Manual/Navigation.html了解相关信息。
NOTE In this script, the object moves back and forth in a straight line, but video games commonly have objects moving around complex paths. Unity comes with support for complex navigation and paths; see https://docs.unity3d .com/Manual/Navigation.html to read about it.
我相信,此时此刻,您一定迫不及待地想要将自己的想法付诸实践,为这款示例游戏添加更多内容。您应该这样做——您可以创建更多艺术资产,甚至通过引入第 3 章中开发的射击机制来测试您的技能。在下一章中,我们将切换到不同的游戏类型,重新开始一款新游戏。尽管未来的章节将切换到其他游戏类型,但前四章中的所有内容仍将适用,并且是有用。
I’m sure that, at this point, you’re itching to apply your own ideas and add more content to this sample game. You should do that—you could create more art assets, or even test your skills by bringing in the shooting mechanics developed in chapter 3. In the next chapter, we’ll switch gears to a different game genre and start over with a new game. Even though future chapters will switch to other game genres, everything from these first four chapters will still apply and be useful.
您已经在 Unity 中构建了第一个游戏原型,因此现在您已准备好通过处理其他游戏类型来拓展自己的能力。此时,您应该对在 Unity 中工作的节奏感到熟悉:创建具有这样或那样功能的脚本,将此对象拖到 Inspector 中的那个插槽,等等。您不再需要过多地纠结于界面的细节,这意味着其余章节不需要重新讨论基础知识。让我们通过一系列额外的项目逐步教您越来越多有关在 Unity 中开发游戏的知识。
You’ve built your first game prototypes in Unity, so now you’re ready to stretch yourself by tackling other game genres. At this point, the rhythms of working in Unity should feel familiar: create a script with such and such function, drag this object to that slot in the Inspector, and so forth. You’re not tripping over details of the interface so much anymore, which means the remaining chapters don’t need to rehash the basics. Let’s run through a succession of additional projects that will progressively teach you more and more about developing games in Unity.
到目前为止,我们一直在使用 3D 图形,但您也可以在 Unity 中使用 2D 图形。因此,在本章中,您将构建一个 2D 游戏。您将开发经典的儿童游戏《记忆》:您将显示一个卡片背面的网格,单击时显示卡片正面,并为匹配得分。这些机制涵盖了在 Unity 中开发 2D 游戏所需了解的基础知识。
Up to now, we’ve been working with 3D graphics, but you can also work with 2D graphics in Unity. So in this chapter, you’ll build a 2D game. You’re going to develop the classic children’s game Memory: you’ll display a grid of card backs, reveal the card front when it’s clicked, and score matches. These mechanics cover the basics you need to know to develop 2D games in Unity.
尽管 Unity 最初是用于 3D 游戏的工具,但它也经常用于 2D 游戏。自 2013 年的 4.3 版以来,Unity 就内置了 2D 图形支持,但在此之前,2D 游戏已经在 Unity 中开发(尤其是利用 Unity 跨平台特性的移动游戏)。在早期版本的 Unity 中,游戏开发人员需要第三方框架来模拟 Unity 3D 场景中的 2D 图形。最终,核心编辑器和游戏引擎进行了修改以纳入 2D 图形,本章将向您介绍该功能。
Although Unity originated as a tool for 3D games, it’s used often for 2D games as well. Unity has had built-in 2D graphics support since version 4.3 in 2013, but even before then 2D games were already being developed in Unity (especially mobile games that took advantage of Unity’s cross-platform nature). In prior versions of Unity, game developers required a third-party framework to emulate 2D graphics within Unity’s 3D scenes. Eventually, the core editor and game engine were modified to incorporate 2D graphics, and this chapter will teach you about that functionality.
Unity 中的 2D 工作流程与开发 3D 游戏的工作流程大致相同:导入艺术资产、将其拖入场景,然后编写脚本以附加到对象。2D 图形中的主要艺术资产类型称为精灵。
The 2D workflow in Unity is more or less the same as the workflow to develop a 3D game: import art assets, drag them into a scene, and write scripts to attach to the objects. The primary kind of art asset in 2D graphics is called a sprite.
定义 精灵是直接显示在屏幕上的二维图像,与显示在三维模型表面的图像(即纹理)相对。
DEFINITION Sprites are 2D images displayed directly on the screen, as opposed to images displayed on the surface of 3D models (that is, textures).
您可以将 2D 图像作为精灵导入 Unity,其方式与将图像作为纹理导入的方式大致相同(请参阅第 4 章)。从技术上讲,这些精灵将是 3D 空间中的对象,但它们将是垂直于 z 轴的平面。由于它们都面向同一方向,您可以将相机直接对准精灵,玩家将只能沿 x 轴和 y 轴(二维)辨别它们的运动。
You can import 2D images into Unity as sprites in much the same way you can import images as textures (see chapter 4). Technically, these sprites will be objects in 3D space, but they’ll be flat surfaces all oriented perpendicular to the z-axis. Because they’ll all face the same direction, you can point the camera straight at the sprites, and players will be able to discern their movements only along the x- and y-axes (in two dimensions).
在第 2 章中,我们讨论了坐标轴:三维空间中除了您熟悉的 x 轴和 y 轴外,还增加了一个 z 轴。二维空间中只有 x 轴和 y 轴(这就是您的老师在数学课上讲的!)。
In chapter 2, we discussed the coordinate axes: having three dimensions adds a z-axis perpendicular to the x- and y-axes you were already familiar with. Two dimensions are just those x- and y-axes (that’s what your teacher was talking about in math class!).
你准备创建经典的记忆游戏。对于那些不熟悉此游戏的人来说,一系列牌面朝下发放。每张牌都有一张匹配的牌位于其他地方,但玩家只能看到牌的反面。玩家可以一次翻开两张牌,尝试找到匹配的牌;如果选择的两张牌不匹配,它们会翻回来,然后玩家可以再次猜测。
You’re going to create the classic game of Memory. For those unfamiliar with this game, a series of cards are dealt out facedown. Every card has a matching card located somewhere else, but the player sees only the reverse side of the card. The player can turn over two cards at a time, attempting to find matching cards; if the two cards chosen aren’t a match, they’ll flip back, and then the player can guess again.
图 5.1 显示了我们将要构建的游戏的模型;将其与第 2 章中的路线图进行比较。这次的模型准确描绘了玩家将看到的内容(而我们的 3D 场景的模型描绘了玩家周围的空间以及摄像机的位置,以便玩家可以透过它们看到)。现在您知道要构建什么了,是时候开始工作了!
Figure 5.1 shows a mock-up of the game we’re going to build; compare this to the road map diagram from chapter 2. The mock-up this time depicts exactly what the player will see (whereas the mock-up for our 3D scene depicted the space around the player and then where the camera went for the player to see through). Now that you know what you’ll be building, it’s time to get to work!
Figure 5.1 Mock-up of what the Memory game will look like
这第一步是收集并显示游戏的图形。与之前构建 3D 演示的方法非常相似,您需要通过组合游戏运行所需的最少图形集来启动新游戏,然后就可以开始对功能进行编程了。
The first step is to gather up and display graphics for our game. In much the same way as building the 3D demo previously, you want to start the new game by putting together the minimum set of graphics for the game to operate, and after that’s in place, you can start programming the functionality.
这意味着您需要创建图 5.1 中所示的所有内容:隐藏卡牌的卡牌背面、翻开卡牌时的一系列卡牌正面、一角的分数显示以及对角的重置按钮。我们还需要一个屏幕背景,因此,我们的艺术要求加起来就是图 5.2。
That means you’ll need to create everything depicted in figure 5.1: card backs for hidden cards, a series of card fronts for when they turn over, a score display in one corner, and a reset button in the opposite corner. We also need a background for the screen, so all together, our art requirements sum up to figure 5.2.
Figure 5.2 Art assets required for the Memory game
提示与往常一样,可以从本书的网站http://mng.bz/VBY5下载项目的完成版本,包括所有必要的艺术资产。您可以从那里复制图像以用于您自己的项目。
TIP As always, a finished version of the project, including all necessary art assets, can be downloaded from http://mng.bz/VBY5, this book’s website. You can copy the images from there to use in your own project.
收集所需的图像,然后在 Unity 中创建一个新项目。在出现的“新项目”窗口中,您会注意到项目模板(如图 5.3 所示),它允许您在 2D 和 3D 模式之间切换。在之前的章节中,我们使用了 3D 图形,由于这是默认值,因此我们不必关心此设置。但在本章中,您需要在创建新项目时选择 2D 模板。
Gather the required images and then create a new project in Unity. In the New Project window that comes up, you’ll notice project templates (shown in figure 5.3) that let you switch between 2D and 3D mode. In previous chapters, we’ve worked with 3D graphics, and because that’s the default value, we haven’t been concerned with this setting. In this chapter, though, you’ll want to select the 2D template when creating a new project.
图 5.3 使用这些按钮在 2D 或 3D 模式下创建新项目。
Figure 5.3 Create new projects in either 2D or 3D mode with these buttons.
随着本章的新项目创建并设置为 2D,我们可以开始将图像放入场景。
With the new project for this chapter created and set for 2D, we can start putting our images into the scene.
拖将所有图像文件拖到“项目”视图中以导入它们,确保图像作为精灵而不是纹理导入。(如果编辑器设置为 2D,则此操作是自动的。选择一个资产以在检查器中查看其导入设置。)现在将table_top精灵拖到(我们的背景图像)从 Project 视图移到空场景中。与网格对象一样,在 Inspector 中有一个用于精灵的 Transform 组件;输入0 , 0 , 5来定位背景图像。
Drag all the image files into the Project view to import them, ensuring that the images are imported as sprites and not textures. (This is automatic if the editor is set to 2D. Select an asset to see its import settings in the Inspector.) Now drag the table_top sprite (our background image) up from the Project view into the empty scene. As with mesh objects, in the Inspector there’s a Transform component for the sprite; type 0, 0, 5 to position the background image.
提示另一个需要注意的导入设置是“每单位像素数”。因为 Unity 以前是一个移植了 2D 图形的 3D 引擎,所以 Unity 中的一个单位不一定是图像中的一个像素。您可以将“每单位像素数”设置为 1:1,但我建议将其保留为默认值 100:1(因为物理引擎在 1:1 时无法正常工作,而默认值更有利于与其他代码的兼容性)。
TIP Another import setting to take note of is Pixels Per Unit. Because Unity was previously a 3D engine that had 2D graphics grafted in, one unit in Unity isn’t necessarily one pixel in the image. You could set the Pixels Per Unit setting to 1:1, but I recommend leaving it at the default of 100:1 (because the physics engine doesn’t work properly at 1:1, and the default is better for compatibility with others’ code).
X 和 Y 位置的 0很简单(这个精灵将填满整个屏幕,所以您希望它位于中心),但Z 位置的5可能看起来很奇怪。对于 2D 图形,难道只有 X 和 Y 重要吗?好吧,X 和 Y 是将对象定位在 2D 屏幕上的唯一重要值,但 Z 值对于堆叠对象仍然很重要。
The 0s for the X and Y positions are straightforward (this sprite will fill the entire screen, so you want it at the center), but that 5 for the Z position might seem odd. For 2D graphics, shouldn’t only X and Y matter? Well, X and Y are the only values that matter for positioning the object on the 2D screen, but the Z value still matters for stacking objects.
Z 值越低,距离相机越近,因此 Z 值较低的精灵会显示在其他精灵之上(参见图 5.4)。因此,背景精灵应该具有最高的 Z 值。您需要将背景设置为正 Z 位置,然后将其他所有内容设置为0或负 Z 位置。
Lower Z values are closer to the camera, so sprites with lower Z values are displayed on top of other sprites (refer to figure 5.4). Accordingly, the background sprite should have the highest Z value. You’ll set your background to a positive Z position, and then give everything else a 0 or negative Z position.
Figure 5.4 How sprites stack along the z-axis
由于前面提到的“每单位像素数”设置,其他精灵将使用最多两位小数的值进行定位。100:1 的比例意味着图像中的 100 个像素在 Unity 中为 1 个单位;换句话说,1 个像素为 0.01 个单位。但在将更多精灵放入场景之前,让我们为此设置相机游戏。
Other sprites will be positioned with values with up to two decimal places because of the Pixels Per Unit setting mentioned earlier. A ratio of 100:1 means that 100 pixels in the image are 1 unit in Unity; put another way, 1 pixel is 0.01 units. But before you put any more sprites into the scene, let’s set up the camera for this game.
现在让我们调整场景中主摄像头的设置。您可能会认为,由于场景视图设置为 2D,因此您在 Unity 中看到的内容就是您在游戏中看到的内容。然而,有些不直观的是,事实并非如此。
Now let’s adjust settings on the main camera in the scene. You might think that because the Scene view is set to 2D, what you see in Unity is what you’ll see in the game. Somewhat unintuitively, though, that isn’t the case.
警告场景视图是否设置为 2D 与正在运行的游戏中的摄像机视图无关。
WARNING Whether or not the Scene view is set to 2D has nothing to do with the camera view in the running game.
事实证明,无论场景视图是否设置为 2D 模式,游戏中的相机都是独立设置的。这在许多情况下都很方便,这样您就可以将场景视图切换回 3D 以在场景中处理某些效果。这种脱节确实意味着您在 Unity 中看到的内容不一定是您在游戏中看到的内容,初学者很容易忘记这一点。
It turns out that, regardless of whether the Scene view is set to 2D mode, the camera in the game is set independently. This can be handy in many situations so that you can toggle the Scene view back to 3D to work on certain effects within the scene. This disconnect does mean that what you see in Unity isn’t necessarily what you see in the game, and it can be easy for beginners to forget this.
要调整的最重要的相机设置是投影。由于您是在 2D 模式下创建新项目的,因此相机投影可能已经正确,但了解这一点仍然很重要,值得仔细检查。在层次结构中选择相机以在检查器中显示其设置,然后查找投影设置(见图 5.5)。对于 3D 图形,设置应为透视,但对于 2D 图形,相机投影应为正交。
The most important camera setting to adjust is Projection. The camera projection is probably already correct because you created the new project in 2D mode, but this is still important to know about and worth double-checking. Select the camera in Hierarchy to show its settings in the Inspector, and then look for the Projection setting (see figure 5.5). For 3D graphics, the setting should be Perspective, but for 2D graphics, the camera projection should be Orthographic.
Figure 5.5 Camera settings to adjust for 2D graphics
定义 正字法是指没有明显透视的平面相机视图。这与透视相机视图相反,在透视相机视图中,较近的物体看起来较大,线条向远处后退。
DEFINITION Orthographic is the term for a flat camera view that has no apparent perspective. This is the opposite of a Perspective camera view, in which closer objects appear larger and lines recede into the distance.
虽然投影模式是 2D 图形最重要的相机设置,但我们还有其他一些设置需要调整。接下来,我们将查看投影下的“大小”。相机的正交尺寸决定了从屏幕中心到屏幕顶部的相机视图的大小。换句话说,将“大小”设置为所需屏幕像素尺寸的一半。如果您稍后将部署游戏的分辨率设置为相同的像素尺寸,您将获得像素完美的图形。
Although the Projection mode is the most important camera setting for 2D graphics, we have a few other settings to adjust as well. Next, we’ll look at Size, which is under Projection. The camera’s orthographic size determines the size of the camera view from the center of the screen up to the top of the screen. In other words, set Size to half the pixel dimensions of the screen you want. If you later set the resolution of the deployed game to the same pixel dimensions, you’ll get pixel-perfect graphics.
定义 像素完美意味着屏幕上的一个像素对应图像中的一个像素(否则,视频卡在放大以适合屏幕时会使图像变得模糊)。
DEFINITION Pixel-perfect means one pixel on the screen corresponds to one pixel in the image (otherwise, the video card will make the images subtly blurry while scaling up to fit the screen).
假设您想要一个像素完美的 1024 × 768 屏幕。这意味着相机高度应该是 384 像素。将其除以 100(因为像素到单位的比例),您将得到相机尺寸 3.84。同样,该数学运算是SCREEN_SIZE / 2 / 100f(f是浮点数,而不是int值)。假设背景图像是 1024 × 768(选择资产以检查其尺寸),那么显然这个 3.84 的值就是我们想要的相机值。
Let’s say you want a pixel-perfect 1024 × 768 screen. That means the camera height should be 384 pixels. Divide that by 100 (because of the pixels-to-units scale) and you get 3.84 for the camera size. Again, that math is SCREEN_SIZE / 2 / 100f (f as in float, rather than an int value). Given that the background image is 1024 × 768 (select the asset to check its dimensions), then clearly this value of 3.84 is what we want for our camera.
在 Inspector 中需要进行的其他调整是相机的背景颜色和 Z 位置。如前所述,对于精灵,Z 位置越高,场景越远。因此,相机的 Z 位置应该很低;将相机的位置设置为0、0、-100。接下来,确保将相机的 Clear Flag 设置为 Solid Color 而不是 Skybox;此设置决定相机的背景。相机的背景颜色应该是黑色;默认颜色是蓝色,如果屏幕比背景图像宽(很可能如此),则沿侧面显示会看起来很奇怪。单击 Background 旁边的颜色样本,并将颜色选择器设置为黑色。
The remaining adjustments to make in the Inspector are the camera’s background color and Z position. As mentioned previously for sprites, higher Z positions are further away into the scene. As such, the camera should have a pretty low Z position; set the position of the camera to 0, 0, -100. Next make sure the camera’s Clear Flag is set to Solid Color instead of Skybox; this setting determines the camera background. The camera’s background color should probably be black; the default color is blue, and that’ll look odd displayed along the sides if the screen is wider than the background image (which is likely). Click the color swatch next to Background and set the color picker to black.
现在将场景保存为Scene并单击 Play。您将看到 Game 视图中充满了我们的桌面精灵。如您所见,到达这一点并不完全简单(再次强调,这是因为 Unity 是一个 3D 游戏引擎,最近才将 2D 图形嫁接进来)。但是桌面完全是空的,所以我们的下一步是放一张卡片这桌子。
Now save the scene as Scene and click Play. You’ll see the Game view filled with our tabletop sprite. As you saw, getting to this point wasn’t completely straightforward (again, that’s because Unity was a 3D game engine that has recently had 2D graphics grafted in). But the tabletop is completely bare, so our next step is to put a card on the table.
现在所有图像都已导入并可供使用,让我们来构建构成此游戏核心的卡片对象。在 Memory 中,所有卡片最初都是面朝下的,当您选择一对卡片翻转时,它们只会暂时面朝上。要实现此功能,您将创建由多个堆叠在一起的精灵组成的对象。然后,您将编写代码,使卡片在用鼠标单击时显示出来。
Now that the images are all imported and ready to use, let’s build the card objects that form the core of this game. In Memory, all the cards are initially face down, and they’re face up only temporarily, when you choose a pair of cards to turn over. To implement this functionality, you’re going to create objects that consist of multiple sprites stacked on top of one another. Then, you’ll write code that makes the cards reveal themselves when clicked with the mouse.
拖将其中一张卡片图像放入场景中。使用其中一张卡片正面,因为您将在顶部添加一张卡片背面来隐藏图像。从技术上讲,现在的位置并不重要,但最终会很重要,因此您也可以将卡片定位在-3 , 1 , 0处。现在拖动card_back精灵进入场景。将这个新精灵设为前一个卡片精灵的子精灵(记住,在层次结构中,将子对象拖到父对象上),然后将其位置设置为0 , 0 , -0.1(请记住,此位置是相对于父对象的,因此这意味着“将其放在相同的 X 和 Y 上,但在 Z 上将其移得更近。”)
Drag one of the card images into the scene. Use one of the card fronts, because you’ll add a card back on top to hide the image. Technically, the position right now doesn’t matter, but eventually it will, so you may as well position the card at -3, 1, 0. Now drag the card_back sprite into the scene. Make this new sprite a child of the previous card sprite (remember, in the Hierarchy, drag the child object onto the parent object) and then set its position to 0, 0, -0.1 (Keep in mind that this position is relative to the parent, so this means, “Put it at the same X and Y but move it closer on Z.”)
注意在此设置中,卡片的背面和正面是单独的对象。这使得图形设置更简单,并且显示“正面”就像关闭“背面”一样简单。但是,由于即使场景看起来是 2D,Unity 也始终是 3D,因此您可以制作一张翻转的 3D 卡片。设置起来会更复杂,但可能对某些图形效果有好处。没有一种正确的方法来实现事物,只是需要平衡不同的利弊。
NOTE In this setup, the back of the card and the front of the card are separate objects. That makes the graphics simpler to set up, and revealing the “front” is as simple as turning off the “back.” However, since Unity is always 3D even when the scene looks 2D, you could make a 3D card that flips over. That would be more complex to set up but may have advantages for certain graphical effects. There’s no one right way to implement things, just different pros and cons to balance.
提示:在 2D 模式下,我们使用一种称为“矩形工具”的操作工具,而不是 3D 模式下的“移动”、“旋转”和“缩放”工具。在 2D 模式下,此工具会自动选中,或者您可以单击 Unity 左上角的第五个控制按钮。激活此工具后,单击并拖动对象即可在二维空间中执行所有三种操作(移动/旋转/缩放)。
TIP Instead of the Move, Rotate, and Scale tools that we used in 3D, in 2D mode we use a single manipulation tool called the Rect tool. In 2D mode, this tool is selected automatically, or you can click the fifth control button in the top-left corner of Unity. With this tool active, click and drag objects to do all three operations (move/rotate/scale) in two dimensions.
将卡片放回原位后,如图 5.6 所示,图形已准备好用于制作反应卡,可以揭晓。
With the card back in place, as depicted in figure 5.6, the graphics are ready for a reactive card that can be revealed.
Figure 5.6 Hierarchy linking and position for the card back sprite
在为了在玩家点击时做出响应,卡片精灵需要有一个碰撞器组件。新精灵默认没有碰撞器,因此无法点击。您将把碰撞器附加到根卡片对象上,但不附加到卡片背面,这样只有卡片正面而不是卡片背面会接收鼠标点击。
In order to respond when the player clicks them, the card sprites need to have a collider component. New sprites don’t have a collider by default, so they can’t be clicked. You’re going to attach a collider to the root card object, but not to the card back, so that only the card front and not the card back will receive mouse clicks.
为此,请在层次结构中选择根卡片对象(不要单击场景中的卡片,因为卡片背面位于顶部,您将选择该部分)。然后单击检查器中的添加组件按钮。选择 Physics 2D(不是 Physics,因为该系统用于 3D 物理,而这是一个 2D 游戏),然后选择一个盒子对撞机。
To do this, select the root card object in Hierarchy (don’t click the card in the scene, because the card back is on top and you’ll select that part instead). Then click the Add Component button in the Inspector. Select Physics 2D (not Physics, because that system is for 3D physics and this is a 2D game) and then choose a box collider.
除了碰撞器之外,卡片还需要一个脚本来对玩家点击它做出反应,所以让我们写一些代码。创建一个名为MemoryCard的新脚本并将其附加到根卡片对象(再次强调,不是卡片背面)。此列表显示了使卡片在被点击时发出调试消息的代码。
Besides a collider, the card needs a script in order to be reactive to the player clicking it, so let’s write some code. Create a new script called MemoryCard and attach it to the root card object (again, not the card back). This listing shows the code that makes the card emit debug messages when clicked.
Listing 5.1 Emitting debug messages when clicked
使用 System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 MemoryCard : MonoBehaviour {
public void OnMouseDown() { ❶
Debug.Log("测试 1 2 3"); ❷
}
}dusing System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MemoryCard : MonoBehaviour {
public void OnMouseDown() { ❶
Debug.Log("testing 1 2 3"); ❷
}
}
❶ The function is called when the object is clicked.
❷ Just emit a test message to the console for now.
提示如果您还没有养成这个习惯,将您的资产组织到单独的文件夹中可能是一个好主意。为脚本创建文件夹并在项目视图中拖动文件。注意避免使用 Unity 响应的特殊文件夹名称:资源、插件、编辑器和 Gizmos。在本书的后面,我们将介绍其中一些特殊文件夹的作用,但现在请避免使用这些词命名任何文件夹。
TIP If you’re not in this habit yet, organizing your assets into separate folders is probably a good idea. Create folders for scripts and drag files within the Project view. Be careful to avoid the special folder names Unity responds to: Resources, Plugins, Editor, and Gizmos. Later in the book, we’ll go over what some of these special folders do, but for now avoid naming any folders with those words.
很好——我们现在可以点击卡片了!就像更新一样(),鼠标按下时()是MonoBehaviour提供的另一个函数,这次在单击对象时做出响应。玩游戏并观察控制台中出现的消息。但这只是为了测试而打印到控制台;我们希望卡片透露。
Nice—we can click the card now! Just like Update(), OnMouseDown() is another function provided by MonoBehaviour, this time responding when the object is clicked. Play the game and watch messages appear in the console. But this only prints to the console for testing; we want the card to be revealed.
Rewrite the code to match this listing (the code won’t run quite yet, but don’t worry).
Listing 5.2 Script that hides the back when the card is clicked
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 MemoryCard : MonoBehaviour {
[SerializeField] GameObject cardBack; ❶
公共无效OnMouseDown(){
如果 (cardBack.activeSelf) { ❷
cardBack.SetActive(false); ❸
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MemoryCard : MonoBehaviour {
[SerializeField] GameObject cardBack; ❶
public void OnMouseDown() {
if (cardBack.activeSelf) { ❷
cardBack.SetActive(false); ❸
}
}
}
❶ Variable that appears in the Inspector
❷ Run deactivate code only if the object is currently active/visible.
❸ Set the object to inactive/invisible.
我们对脚本添加了两个关键内容:对场景中对象的引用和SetActive()方法这会停用该对象。第一部分,即对场景中对象的引用,与我们在前几章中所做的类似:将变量标记为序列化,然后将对象从层次结构拖到检查器中的变量上。设置对象引用后,代码现在将影响场景中的对象。
We’ve made two key additions to the script: a reference to an object in the scene, and the SetActive() method that deactivates that object. The first part, the reference to an object in the scene, is similar to what we’ve done in previous chapters: mark the variable as serialized and then drag the object from Hierarchy over to the variable in the Inspector. With the object reference set, the code will now affect the object in the scene.
代码中的第二个关键添加项是SetActive命令。此命令将停用任何 GameObject,使该对象不可见。如果我们现在将场景中的card_back拖到 Inspector 中此脚本的变量,则在游戏过程中单击卡片时,卡片背面会消失。隐藏卡片背面将显示卡片正面;我们完成了记忆游戏的另一项重要任务!但这仍然只有一张卡片,所以现在让我们创建一堆的牌。
The second key addition to the code is the SetActive command. This command will deactivate any GameObject, making that object invisible. If we now drag card_back in the scene to this script’s variable in the Inspector, the card back disappears when you click the card during a game. Hiding the card back will reveal the card front; we’ve accomplished yet another important task for the Memory game! But this is still only one card, so now let’s create a bunch of cards.
提示:当脚本具有序列化变量时忘记拖动对象是一个相当常见的错误,因此在控制台选项卡中识别该错误消息很有用。使用尚未设置的序列化变量的代码将引发空引用错误。实际上,只要代码尝试使用尚未设置的变量,就会引发空引用错误,无论它是否是序列化变量。
TIP Forgetting to drag over the object when a script has a serialized variable is a fairly common mistake, so it’s useful to recognize that error message in the Console tab. Code that uses a serialized variable that hasn’t been set will throw a null reference error. Actually, a null reference error gets thrown any time the code attempts to use a variable not set yet, whether or not it’s a serialized variable.
我们编写了一个卡片对象,该对象最初显示卡片背面,但在单击时会显示出来。这是一张卡片,但游戏需要一整张卡片网格,大多数卡片上都有不同的图像。我们将使用前几章中看到的几个概念以及一些您以前从未见过的概念来实现卡片网格。第 3 章介绍了使用不可见的SceneController组件的概念并实例化对象的克隆。SceneController这次将把不同的图像应用到不同的卡片上。
We’ve programmed a card object that initially shows the card back but reveals itself when clicked. That was a single card, but the game needs a whole grid of cards, with different images on most cards. We’ll implement the grid of cards by using a couple of concepts seen in previous chapters, along with some concepts you haven’t seen before. Chapter 3 introduced the concepts of using an invisible SceneController component and instantiating clones of an object. The SceneController will apply different images to different cards this time.
这我们正在创建的游戏有四张牌图像。桌上的所有八张牌(每个符号两张)都将通过克隆相同的原始牌来创建,因此所有牌最初都具有相同的符号。我们必须在脚本中更改牌上的图像,以编程方式加载不同的图像。
The game we’re creating has four card images. All eight cards on the table (two for each symbol) will be created by cloning the same original, so all cards will initially have the same symbol. We’ll have to change the image on the card in the script, loading different images programmatically.
为了检查如何以编程方式分配图像,让我们编写简单的测试代码(稍后将被替换)来演示该技术。首先,将此代码添加到MemoryCard脚本中。
To examine how images can be assigned programmatically, let’s write simple test code (which will be replaced later) to demonstrate the technique. First, add this code to the MemoryCard script.
Listing 5.3 Test code to demonstrate changing the sprite image
... [SerializeField] 雪碧图; ❶ 无效开始(){ GetComponent<SpriteRenderer>().sprite = image; ❷ } ...
... [SerializeField] Sprite image; ❶ void Start() { GetComponent<SpriteRenderer>().sprite = image; ❷ } ...
❶ Reference to the Sprite asset that will be loaded
❷ Set the sprite for this SpriteRenderer component.
保存此脚本后,新的图像变量将显示在 Inspector 中,因为它已被设置为序列化。从 Project 视图中向上拖动精灵(选择一张卡片图像,不要与场景中已有的图像相同)并将其放在 Image 插槽中。现在运行场景,您将在卡片上看到新图像。
After you save this script, the new image variable will appear in the Inspector because it has been set as serialized. Drag a sprite up from the Project view (pick one of the card images, and not the same as the image already in the scene) and drop it on the Image slot. Now run the scene and you’ll see the new image on the card.
理解此代码的关键是了解SpriteRenderer组件你会注意到,在图 5.7 中,卡片背面对象只有两个组件:标准Transform组件在场景中的所有对象上,以及一个名为SpriteRenderer的新组件。此组件将 GameObject 变成一个 sprite 对象,并确定将显示哪个 sprite 资源。请注意,组件中的第一个属性称为sprite,并链接到 Project 视图中的一个 sprite;该属性可以在代码中操作,这正是此脚本所做的。
The key to understanding this code is to know about the SpriteRenderer component. You’ll notice in figure 5.7 that the card back object has just two components: the standard Transform component on all objects in the scene, and a new component called SpriteRenderer. This component makes the GameObject into a sprite object and determines which sprite asset will be displayed. Note that the first property in the component is called sprite and links to one of the sprites in the Project view; the property can be manipulated in code, and that’s precisely what this script does.
图 5.7 场景中的精灵对象附加了SpriteRenderer组件。
Figure 5.7 A sprite object in the scene has the SpriteRenderer component attached to it.
与前几章中的CharacterController和自定义脚本一样,GetComponent()方法返回同一对象上的其他组件,因此我们使用它来引用SpriteRenderer对象. sprite属性SpriteRenderer可以设置为任何精灵资产,因此此代码将该属性设置为Sprite变量在顶部声明(我们在编辑器中用精灵资产填充)。
As it did with CharacterController and custom scripts in previous chapters, the GetComponent() method returns other components on the same object, so we use it to reference the SpriteRenderer object. The sprite property of SpriteRenderer can be set to any sprite asset, so this code sets that property to the Sprite variable declared at the top (which we filled with a sprite asset in the editor).
嗯,这并不难!但这只是一张图像。我们有四张图片要使用,所以现在删除清单 5.3 中的新代码(这只是该技术如何工作的演示),为下一步做准备部分。
Well, that wasn’t too hard! But it’s only a single image. We have four images to use, so now delete the new code from listing 5.3 (it was only a demonstration of how the technique works) to prepare for the next section.
记起在第 3 章中,我们在场景中创建了一个不可见对象来控制生成对象。我们在这里也将采用这种方法,使用不可见对象来控制不与场景中任何特定对象绑定的更抽象的功能。
Recall that, in chapter 3, we created an invisible object in the scene to control spawning objects. We’re going to take that approach here as well, using an invisible object to control more abstract features that aren’t tied to any specific object in the scene.
首先,创建一个空的 GameObject(记住,选择 GameObject > Create Empty)。然后在 Project 视图中创建一个新脚本SceneController,并将此脚本资源拖到控制器 GameObject 上。在新脚本中编写代码之前,将下一个清单的内容添加到MemoryCard脚本中,而不是清单 5.3 中看到的内容。
First, create an empty GameObject (remember, choose GameObject > Create Empty). Then create a new script, SceneController, in the Project view, and drag this script asset onto the controller GameObject. Before writing code in the new script, add the contents of the next listing to the MemoryCard script instead of what you saw in listing 5.3.
Listing 5.4 New public methods in MemoryCard
...
[SerializeField]SceneController 控制器;
私有int _id;
公共 int ID {
获取 {return _id;} ❶
}
public void SetCard(int id, Sprite image) { ❷
_id = id;
GetComponent<SpriteRenderer>().sprite = 图像; ❸
}
......
[SerializeField] SceneController controller;
private int _id;
public int Id {
get {return _id;} ❶
}
public void SetCard(int id, Sprite image) { ❷
_id = id;
GetComponent<SpriteRenderer>().sprite = image; ❸
}
...
❶添加了 getter 函数(C# 和 Java 等语言中常见的用法)
❶ Added getter function (an idiom common in languages like C# and Java)
❷ Public method that other scripts can use to pass new sprites to this object
❸ SpriteRenderer 代码行就像删除的代码演示中一样
❸ SpriteRenderer code line just as in the deleted code demonstration
与之前的清单相比,主要的变化是我们现在在SetCard()而不是Start()中设置精灵图像。因为这是一个以精灵为参数的公共方法,所以您可以从其他脚本调用此函数并将图像设置在此对象上。请注意,SetCard()还将 ID 号作为参数,并且代码会存储该号码。虽然我们目前还不需要 ID,但很快我们将编写比较卡片是否匹配的代码,并且该比较将依赖于卡片的 ID。
The primary change from previous listings is that we’re now setting the sprite image in SetCard() instead of Start(). Because that’s a public method that takes a sprite as a parameter, you can call this function from other scripts and set the image on this object. Note that SetCard() also takes an ID number as a parameter, and the code stores that number. Although we don’t need the ID quite yet, soon we’ll write code that compares cards for matches, and that comparison will rely on the IDs of the cards.
注意:根据您过去使用的编程语言,您可能不熟悉getters和setters的概念。长话短说,它们是在您尝试访问与其关联的属性时运行的函数(例如,检索card.Id的值)。使用 getters 和 setters 的原因有很多,但在这种情况下,Id属性是只读的,因为我们有一个函数只能获取值而不设置它。
NOTE Depending on what programming languages you’ve used in the past, you may not be familiar with the concept of getters and setters. Long story short, they are functions that run when you attempt to access the property associated with them (for example, retrieving the value of card.Id). There are multiple reasons to use getters and setters, but in this case the Id property is read-only because we have a function to only get the value and not set it.
最后,请注意代码中有一个控制器变量。即使SceneController开始克隆卡片对象以填充场景,卡片对象也需要对控制器的引用来调用其公共方法。像往常一样,当代码引用场景中的对象时,将 Unity 编辑器中的控制器对象拖到 Inspector 中的序列化变量槽中。对这张卡片执行一次此操作,之后的所有副本也将具有引用。现在,有了MemoryCard中的附加代码,请在SceneController中编写此代码。
Finally, note that the code has a variable for the controller. Even as SceneController starts cloning card objects to fill the scene, the card objects also need a reference to the controller to call its public methods. As usual, when the code references objects in the scene, drag the controller object in Unity’s editor to the serialized variable slot in the Inspector. Do this once for this single card, and all of the copies to come later will have the reference as well. With that additional code now in MemoryCard, write this code in SceneController.
Listing 5.5 First pass at SceneController for the Memory game
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 SceneController : MonoBehaviour {
[SerializeField] MemoryCard originalCard; ❶
[SerializeField] Sprite[] images; ❷
无效开始(){
int id = 随机范围(0,图像长度);
originalCard.SetCard(id,images [id]); ❸
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SceneController : MonoBehaviour {
[SerializeField] MemoryCard originalCard; ❶
[SerializeField] Sprite[] images; ❷
void Start() {
int id = Random.Range(0, images.Length);
originalCard.SetCard(id, images[id]); ❸
}
}
❶ Reference for the card in the scene
❷ An array for references to the sprite assets
❸ Call the public method we added to MemoryCard.
目前,这是一个简短的代码片段,用于演示从SceneController操作卡片的概念。其中大部分内容您应该已经很熟悉了(例如,在 Unity 的编辑器中,将卡片对象拖到 Inspector 中的序列化变量槽中),但图像数组是新的。如图 5.8 所示,您可以在 Inspector 中设置元素的数量。输入4作为数组长度,然后将卡片图像的精灵拖到数组槽中。现在,这些精灵可以在数组中访问,就像任何其他对象引用一样。
For now, this is a short snippet to demonstrate the concept of manipulating cards from SceneController. Most of this should already be familiar to you (for example, in Unity’s editor, drag the card object to the serialized variable slot in the Inspector), but the array of images is new. As shown in figure 5.8, in the Inspector you can set the number of elements. Type in 4 for the array length and then drag the sprites for card images onto the array slots. Now these sprites can be accessed in the array, like any other object reference.
Figure 5.8 The filled-in array of sprites
顺便说一下,我们使用了Random.Range()方法在第 3 章中,希望您还记得这一点。确切的边界值在那里并不重要,但这次需要注意的是,最小值是包含在内并且可以返回,而返回值始终低于最大值。
Incidentally, we used the Random.Range() method in chapter 3, so hopefully you recall that. The exact boundary values didn’t matter there, but this time it’s important to note that the minimum value is inclusive and may be returned, whereas the return value is always below the maximum.
单击“播放”运行此新代码。每次运行场景时,您都会看到不同的图像应用于显示的卡片。下一步是创建一个完整的卡片网格,而不仅仅是一。
Click Play to run this new code. You’ll see different images being applied to the revealed card each time you run the scene. The next step is to create a whole grid of cards instead of only one.
场景控制器 已经有了对卡片对象的引用,所以现在你将使用Instantiate()方法(参见下一个清单)多次克隆对象,就像我们在第 3 章中生成对象时所做的那样。
SceneController already has a reference to the card object, so now you’ll use the Instantiate() method (see the next listing) to clone the object numerous times, as we did when spawning objects in chapter 3.
Listing 5.6 Cloning the card eight times and positioning in a grid
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 SceneController : MonoBehaviour {
公共 const int gridRows = 2; ❶
公共 const int gridCols = 4; ❶
公共 const float offsetX = 2f; ❶
公共 const float offsetY = 2.5f; ❶
[序列化字段] 内存卡原始卡;
[SerializeField] Sprite[] 图像;
无效开始(){
Vector3 startPos = originalCard.transform.position; ❷
对于 (int i = 0; i < gridCols; i++) {
for (int j = 0; j < gridRows; j++) { ❸
MemoryCard 卡; ❹
如果 (i == 0 && j == 0) {
卡 = 原始卡;
} 别的 {
卡 = 实例化(原始卡)作为记忆卡;
}
int id = 随机范围(0,图像长度);
卡片.设置卡片(id,图片[id]);
浮点 posX = (偏移量X * i) + startPos.x;
float posY = -(offsetY * j) + startPos.y;
卡片.变换.位置 = 新向量3(posX,posY,startPos.z);
❺
}
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SceneController : MonoBehaviour {
public const int gridRows = 2; ❶
public const int gridCols = 4; ❶
public const float offsetX = 2f; ❶
public const float offsetY = 2.5f; ❶
[SerializeField] MemoryCard originalCard;
[SerializeField] Sprite[] images;
void Start() {
Vector3 startPos = originalCard.transform.position; ❷
for (int i = 0; i < gridCols; i++) {
for (int j = 0; j < gridRows; j++) { ❸
MemoryCard card; ❹
if (i == 0 && j == 0) {
card = originalCard;
} else {
card = Instantiate(originalCard) as MemoryCard;
}
int id = Random.Range(0, images.Length);
card.SetCard(id, images[id]);
float posX = (offsetX * i) + startPos.x;
float posY = -(offsetY * j) + startPos.y;
card.transform.position = new Vector3(posX, posY, startPos.z);
❺
}
}
}
}
❶ Values for how many grid spaces to make and how far apart to place them
❷ Position of the first card; all other cards will be offset from here.
❸ Nested loops to define both columns and rows of the grid
❹ Container reference for either the original card or the copies
❺对于 2D 图形,仅需要偏移 X 和 Y;保持 Z 相同。
❺ For 2D graphics, you need to offset only X and Y; keep Z the same.
虽然此脚本比上一个清单长得多,但无需解释太多,因为大多数添加的内容都是简单的变量声明和数学运算。此代码中最奇怪的部分可能是以if (i == 0 && j == 0)开头的if/else语句。该条件要么将原始卡片对象用于第一个网格槽,要么将卡片对象克隆用于所有其他网格槽。由于原始卡片已存在于场景中,因此如果在循环的每次迭代中复制卡片,则场景中的卡片会过多。然后根据循环的迭代次数通过偏移卡片来定位卡片。
Although this script is much longer than the previous listing, there’s not a lot to explain because most of the additions are straightforward variable declarations and math. The oddest bit of this code is probably the if/else statement that begins if (i == 0 && j == 0). That conditional either uses the original card object for the first grid slot or clones the card object for all other grid slots. Because the original card already exists in the scene, if you copied the card at every iteration of the loop, you’d end up with one too many cards in the scene. The cards are then positioned by offsetting them according to the number of iterations through the loop.
提示:就像移动 3D 对象一样,2D 对象可以在Update()中反复增加transform.position以实现随时间平稳移动。但正如您在移动第一人称玩家时所看到的,直接调整transform.position时不会应用碰撞检测。因此,下一章的代码将通过调整rigidbody2D.velocity来移动精灵。
TIP Just as when moving 3D objects, 2D objects could have transform.position incremented repeatedly in Update() to achieve smooth movement over time. But as you saw when moving the first-person player, collision detection isn’t applied when adjusting transform.position directly. For that reason, the next chapter’s code will move sprites by adjusting rigidbody2D.velocity.
现在运行代码,将创建一个由八张卡片组成的网格(如图 5.9 所示)。准备卡片网格的最后一步是将它们组织成对,而不是将它们放在一起随机的。
Run the code now, and a grid of eight cards will be created (as depicted in figure 5.9). The last step in preparing the grid of cards is to organize them into pairs instead of keeping them random.
Figure 5.9 The grid of eight cards, which are revealed when you click them
反而为使每张卡牌随机,我们将定义一个包含所有卡牌 ID 的数组(每张卡牌一对,数字0到3各两次),然后打乱该数组。然后我们将在设置卡牌时使用这个卡牌 ID 数组,而不是使每张卡牌随机。
Instead of making every card random, we’ll define an array of all the card IDs (numbers 0 through 3 twice, for a pair of each card) and then shuffle that array. We’ll then use this array of card IDs when setting cards, rather than making each one random.
Listing 5.7 Placing cards from a shuffled list
...
void Start() { ❶
Vector3 startPos = 原始卡片.变换.位置;
int[] numbers = {0, 0, 1, 1, 2, 2, 3, 3}; ❷
numbers = ShuffleArray(numbers); ❸
对于 (int i = 0; i < gridCols; i++) {
对于 (int j = 0; j < gridRows; j++) {
MemoryCard卡;
如果 (i == 0 && j == 0) {
卡 = 原始卡;
} 别的 {
卡 = 实例化(原始卡)作为记忆卡;
}
int 索引 = j * gridCols + i;
int id = 数字[索引]; ❹
卡片.设置卡片(id,图片[id]);
浮点 posX = (偏移量X * i) + startPos.x;
float posY = -(offsetY * j) + startPos.y;
卡片.变换.位置 = 新向量3(posX,posY,startPos.z);
}
}
}
私有 int[] ShuffleArray(int[] numbers) { ❺
int[] newArray = numbers.Clone() 作为 int[];
对于(int i = 0; i < newArray.Length; i ++){
int tmp = newArray[i];
int r = 随机范围(i, newArray.Length);
newArray[i] = newArray[r];
newArray[r] = tmp;
}
返回新数组;
}
......
void Start() { ❶
Vector3 startPos = originalCard.transform.position;
int[] numbers = {0, 0, 1, 1, 2, 2, 3, 3}; ❷
numbers = ShuffleArray(numbers); ❸
for (int i = 0; i < gridCols; i++) {
for (int j = 0; j < gridRows; j++) {
MemoryCard card;
if (i == 0 && j == 0) {
card = originalCard;
} else {
card = Instantiate(originalCard) as MemoryCard;
}
int index = j * gridCols + i;
int id = numbers[index]; ❹
card.SetCard(id, images[id]);
float posX = (offsetX * i) + startPos.x;
float posY = -(offsetY * j) + startPos.y;
card.transform.position = new Vector3(posX, posY, startPos.z);
}
}
}
private int[] ShuffleArray(int[] numbers) { ❺
int[] newArray = numbers.Clone() as int[];
for (int i = 0; i < newArray.Length; i++ ) {
int tmp = newArray[i];
int r = Random.Range(i, newArray.Length);
newArray[i] = newArray[r];
newArray[r] = tmp;
}
return newArray;
}
...
❶ Much of this listing is context to show where the additions go.
❷ Declare an integer array with a pair of IDs for all four card sprites.
❸ Call a function that will shuffle the elements of the array.
❹ Retrieve IDs from the shuffled list instead of random numbers.
❺ An implementation of the Knuth shuffle algorithm
现在,当你点击“播放”时,纸牌网格将是一个打乱的组合,每张纸牌图像正好显示两张。纸牌阵列经过Knuth(也称为Fisher-Yates)随机排序算法,一种简单但有效的数组元素随机排序方法。此算法循环遍历数组,并将数组的每个元素与另一个随机选择的数组位置交换。
Now, when you click Play, the grid of cards will be a shuffled assortment that reveals exactly two of each card image. The array of cards was run through the Knuth (also known as Fisher-Yates) shuffle algorithm, a simple yet effective way of shuffling the elements of an array. This algorithm loops through the array and swaps every element of the array with another randomly chosen array position.
你可以点击所有卡片来显示它们,但记忆游戏应该成对进行。我们需要一点更多的代码。
You can click all the cards to reveal them, but the game of Memory is supposed to proceed in pairs. We need a bit more code.
这制作功能齐全的记忆游戏的最后一步是检查匹配情况。虽然我们现在有一张卡片网格,点击后会显示出来,但各种卡片不会以任何方式相互影响。在记忆游戏中,每次显示一对卡片时,我们都应该检查显示的卡片是否匹配。
The last step in making a fully functional Memory game is checking for matches. Although we now have a grid of cards that are revealed when clicked, the various cards don’t affect each other in any way. In the game of Memory, every time a pair of cards is revealed, we should check to see if the revealed cards match.
这种抽象逻辑(检查匹配并做出适当响应)要求卡片在被点击时通知SceneController 。这需要在下一个清单中显示的SceneController中添加内容。
This abstract logic—checking for matches and responding appropriately—requires that cards notify SceneController when they’ve been clicked. That requires the additions to SceneController shown in the next listing.
清单 5.8 SceneController,必须跟踪显示的卡片
Listing 5.8 SceneController, which must keep track of revealed cards
...
私人记忆卡首次披露;
私人记忆卡secondRevealed;
公共布尔可以显示{
获取 {return secondRevealed == null;} ❶
}
...
public void CardRevealed(MemoryCard 卡){
// 初始为空
}
......
private MemoryCard firstRevealed;
private MemoryCard secondRevealed;
public bool canReveal {
get {return secondRevealed == null;} ❶
}
...
public void CardRevealed(MemoryCard card) {
// initially empty
}
...
❶如果第二张牌已经显示,则返回 false 的 getter 函数
❶ Getter function that returns false if a second card is already revealed
CardRevealed ()方法会立即填充;我们现在需要空的脚手架,以便在MemoryCard中引用,而不会出现任何编译器错误。请注意,我们再次有一个只读的 getter,这次用于确定是否可以显示另一张牌。只有当两张牌尚未显示时,玩家才能显示另一张牌。
The CardRevealed() method will be filled in momentarily; we need the empty scaffolding for now to refer to in MemoryCard without any compiler errors. Note that we have a read-only getter again, this time used to determine whether another card can be revealed. The player can reveal another card only when two cards aren’t already revealed.
我们还需要修改MemoryCard以调用(当前为空)方法,以便在单击卡片时通知SceneController 。根据此清单修改MemoryCard中的代码。
We also need to modify MemoryCard to call the (currently empty) method in order to inform SceneController when a card is clicked. Modify the code in MemoryCard according to this listing.
Listing 5.9 MemoryCard modifications for revealing cards
...
公共无效OnMouseDown(){
如果 (cardBack.activeSelf && controller.canReveal) { ❶
cardBack.设置Active(false);
控制器.CardRevealed(这个); ❷
}
}
公共无效揭示(){ ❸
cardBack.设置活动(true);
}
......
public void OnMouseDown() {
if (cardBack.activeSelf && controller.canReveal) { ❶
cardBack.SetActive(false);
controller.CardRevealed(this); ❷
}
}
public void Unreveal() { ❸
cardBack.SetActive(true);
}
...
❶检查控制器的 canReveal 属性,确保每次只显示两张卡片。
❶ Check the controller’s canReveal property to make sure only two cards are revealed at a time.
❷ Notify the controller when this card is revealed.
❸一个公共方法,以便SceneController可以再次隐藏卡片(通过重新打开card_back)
❸ A public method so that SceneController can hide the card again (by turning card_back back on)
如果您在CardRevealed()中放入调试语句来测试对象之间的通信,则每当您单击一张卡片时,您都会看到测试消息出现。让我们首先处理一对已显示的卡片。
If you were to put a debug statement inside CardRevealed() to test the communication between objects, you’d see the test message appear whenever you click a card. Let’s first handle one revealed pair.
这卡片对象被传递到了CardRevealed()中,所以让我们开始追踪已显示的卡片。
The card object was passed into CardRevealed(), so let’s start keeping track of the revealed cards.
清单 5.10 在SceneController中跟踪显示的卡片
Listing 5.10 Keeping track of revealed cards in SceneController
...
public void CardRevealed(MemoryCard 卡){
如果 (firstRevealed == null) { ❶
首次揭晓 = 卡片;
} 别的 {
第二次揭晓 = 卡片;
Debug.Log("匹配吗? " + (firstRevealed.Id == secondRevealed.Id)); ❷
}
}
......
public void CardRevealed(MemoryCard card) {
if (firstRevealed == null) { ❶
firstRevealed = card;
} else {
secondRevealed = card;
Debug.Log("Match? " + (firstRevealed.Id == secondRevealed.Id)); ❷
}
}
...
❶将卡片对象存储在两个卡片变量之一中,取决于第一个变量是否已被占用。
❶ Store card objects in one of the two card variables, depending on whether the first variable is already occupied.
❷ Compare the IDs of the two revealed cards.
清单将显示的卡片存储在两个卡片变量之一中,具体取决于第一个变量是否已被占用。如果第一个变量为空,则填充它;如果它已被占用,则填充第二个变量并检查卡片 ID 是否匹配。Debug语句在控制台中打印true或false 。
The listing stores the revealed cards in one of the two card variables, depending on whether the first variable is already occupied. If the first variable is empty, fill it; if it’s already occupied, fill the second variable and check the card IDs for a match. The Debug statement prints either true or false in the console.
目前,代码不会对匹配做出响应——它只会检查匹配。现在让我们编写回复。
At the moment, the code doesn’t respond to matches—it only checks for them. Now let’s program the response.
出色地再次使用协程,因为对不匹配的卡片的反应应该暂停,以允许玩家看到卡片。请参阅第 3 章以了解协程的完整解释;长话短说,使用协程将允许我们在检查匹配之前暂停。此清单显示了更多可添加到SceneController的代码。
We’ll use coroutines again because the reaction to mismatched cards should pause to allow the player to see the cards. Refer to chapter 3 for a full explanation of coroutines; long story short, using a coroutine will allow us to pause before checking for a match. This listing shows more code for you to add to SceneController.
清单 5.11 SceneController对比赛进行评分或隐藏错过的比赛
Listing 5.11 SceneController scores a match or hides missed matches
... 私有 int 分数 = 0; ❶ ... public void CardRevealed(MemoryCard 卡){ 如果 (firstRevealed == null) { 首次揭晓 = 卡片; } 别的 { 第二次揭晓 = 卡片; 启动协同程序(CheckMatch()); ❷ } } 私有 IEnumerator CheckMatch() { 如果 (firstRevealed.Id == secondRevealed.Id) { 分数++; ❸ Debug.Log($"分数:{score}"); ❹ } 别的 { 产生返回新的WaitForSeconds(.5f); firstRevealed.Unreveal(); ❺ 第二个显示.取消显示(); } firstRevealed = null; ❻ 第二个显示 = 空; } ...
... private int score = 0; ❶ ... public void CardRevealed(MemoryCard card) { if (firstRevealed == null) { firstRevealed = card; } else { secondRevealed = card; StartCoroutine(CheckMatch()); ❷ } } private IEnumerator CheckMatch() { if (firstRevealed.Id == secondRevealed.Id) { score++; ❸ Debug.Log($"Score: {score}"); ❹ } else { yield return new WaitForSeconds(.5f); firstRevealed.Unreveal(); ❺ secondRevealed.Unreveal(); } firstRevealed = null; ❻ secondRevealed = null; } ...
❶ Add to the list near the top of SceneController
❷ The only changed line in this function, which calls the coroutine when both cards are revealed
❸ Increment the score if the revealed cards have matching IDs.
❹ Construct the message by using string interpolation.
❺ Unreveal the cards if they do not match.
❻ Clear out the variables whether or not a match was made.
首先,添加要跟踪的分数值。然后,当第二张牌被揭开时,启动一个协程来执行CheckMatch()。该协程有两个代码路径,具体取决于牌是否匹配。如果匹配,协程不会暂停;yield命令被跳过。如果卡片不匹配,协程会暂停半秒钟,然后对两张卡片调用Unreveal(),再次隐藏它们。最后,无论是否匹配,用于存储卡片的变量都会被清零,为显示更多卡片铺平道路。
First, add a score value to track. Then, launch a coroutine to CheckMatch() when a second card is revealed. That coroutine has two code paths, depending on whether the cards match. If they match, the coroutine doesn’t pause; the yield command gets skipped over. If the cards don’t match, the coroutine pauses for half a second before calling Unreveal() on both cards, hiding them again. Finally, whether or not a match was made, the variables for storing cards are both nulled out, paving the way for revealing more cards.
当你玩游戏时,不匹配的卡牌会短暂显示,然后再次隐藏。当你对匹配进行评分时会出现调试消息,但我们希望将得分显示为标签屏幕。
When you play the game, mismatched cards will display briefly before hiding again. Debug messages appear when you score matches, but we want the score displayed as a label on the screen.
显示向玩家提供信息是游戏 UI 存在的原因的一半(另一半是接收来自玩家的输入。UI 按钮将在下一节中讨论)。
Displaying information to the player is half of the reason for a UI in a game (the other half is receiving input from the player. UI buttons are discussed in the next section).
定义 UI代表用户界面。另一个密切相关的术语是GUI,即图形用户界面,指的是界面的可视部分,例如文本和按钮,很多人说的 UI 就是这个意思。
DEFINITION UI stands for user interface. Another closely related term is GUI, or graphical user interface, which refers to the visual part of the interface, such as text and buttons, and which is what a lot of people mean when they say UI.
Unity 有多种创建文本显示的方法,但使用 TextMeshPro 包是最好的方法。这个先进的文本系统是外部开发的,后来被 Unity 收购。
Unity has multiple ways to create text displays, but using the TextMeshPro package is the best approach. This advanced text system was developed externally and later acquired by Unity.
TextMeshPro 可能已经安装(创建新项目时,Unity 会安装几个常用包),但如果没有,则必须在包管理器中安装它。从菜单中,选择窗口 > 包管理器以打开该窗口,然后向下滚动到左侧列表中的 TextMeshPro,如图 5.10 所示。选择该包,然后单击安装按钮。
TextMeshPro may already be installed (when creating a new project, Unity installs several commonly used packages), but if not, then you must install it in Package Manager. From the menu, choose Window > Package Manager to open that window and scroll down to TextMeshPro in the list on the left, as shown in figure 5.10. Select that package and then click the Install button.
Figure 5.10 Installing TextMeshPro via Package Manager
安装该软件包后,您可以通过转到 GameObject 菜单并选择 3D Object > Text - TextMeshPro 在场景中创建 TextMeshPro 对象。由于这是第一次在此项目中使用 TextMeshPro,因此会自动出现 TMP Importer 窗口。单击 Import TMP Essentials 按钮,在所需资源下载完成后,文本对象将出现在场景中。
With that package installed, you can create a TextMeshPro object in the scene by going to the GameObject menu and choosing 3D Object > Text - TextMeshPro. Since this will be the first time TextMeshPro is used in this project, the TMP Importer window will automatically appear. Click the Import TMP Essentials button, and after the required resources finish downloading, the text object will appear in the scene.
注意3D 文本可能听起来与 2D 游戏不兼容,但别忘了这在技术上是一个 3D 场景,因为它是通过正交相机看到的,所以看起来是平面的。这意味着我们可以根据需要将 3D 对象放入 2D 游戏中 — 它们以平面视角显示。
NOTE 3D text might sound incompatible with a 2D game, but don’t forget that this is technically a 3D scene that looks flat because it’s being seen through an orthographic camera. That means we can put 3D objects into the 2D game if we want—they’re displayed in a flat perspective.
警告TextMeshPro 也列在 GameObject > UI 下。后面的章节将介绍 Unity 的 UI 系统,您将在那些章节中使用其他 GameObject。不要混淆这两个版本;虽然两者都是 TextMeshPro 对象,但在本章中我们不会使用 Unity 的高级 UI 系统。
WARNING TextMeshPro is also listed under GameObject > UI. Later chapters cover Unity’s UI system, and you’ll use that other GameObject instead in those chapters. Don’t get the two versions confused; while both are TextMeshPro objects, we are not using Unity’s advanced UI system in this chapter.
选择新的文本对象以在 Inspector 中查看其设置。将此对象定位在-2.3、3.1、-10处;即左侧 230 像素和上方 310 像素,将其放在左上角并靠近摄像头,以便它出现在其他游戏对象的顶部。同时将宽度减小到5,高度减小到1,因为新文本一开始很大。
Select the new text object to see its settings in the Inspector. Position this object at -2.3, 3.1, -10; that’s 230 pixels to the left and 310 pixels up, putting it in the top-left corner and nearer to the camera so that it’ll appear on top of other game objects. Also decrease Width to 5 and Height to 1 since the new text starts out huge.
向下滚动到 TextMeshPro 设置。我们可以用很多方法自定义文本,但现在将保留大部分默认设置。图 5.11 显示了我们将要更改的设置,您可以在 Unity 文档 ( http://mng.bz/RqQP ) 中了解所有这些设置。
Scroll down to the TextMeshPro settings. We could customize the text in tons of ways but are going to leave most of the defaults for now. Figure 5.11 shows the settings we’ll change, and you can learn about them all in the Unity documentation (http://mng.bz/RqQP).
Figure 5.11 Inspector settings for this text object
在大文本输入框中输入Score:并将 Font Size 减小到8。在游戏过程中操作此文本对象只需在计分代码中进行一些调整。
Enter Score: in the big Text Input box and decrease Font Size to 8. Manipulating this text object during the game requires just a few adjustments in the scoring code.
Listing 5.12 Displaying the score on a text object
...
使用TMPro; ❶
...
[序列化字段] TMP_Text 分数标签;
...
私有 IEnumerator CheckMatch() {
如果 (firstRevealed.Id == secondRevealed.Id) {
分数++;
scoreLabel.text = $"分数:{score}";
}
......
using TMPro; ❶
...
[SerializeField] TMP_Text scoreLabel;
...
private IEnumerator CheckMatch() {
if (firstRevealed.Id == secondRevealed.Id) {
score++;
scoreLabel.text = $"Score: {score}";
}
...
如您所见,文本是对象的一个属性,您可以将其设置为新字符串。将分数变量放入字符串中来显示该值。
As you can see, text is a property of the object that you can set to a new string. Put the score variable into the string to display that value.
将场景中的文本对象拖拽到scoreLabel变量中添加到SceneController中,然后单击“播放”。现在,您应该看到在玩游戏和匹配时显示的分数。好极了——游戏作品!
Drag the text object in the scene to the scoreLabel variable you added to SceneController and then click Play. Now you should see the score displayed while you play the game and make matches. Huzzah—the game works!
在此时,记忆游戏已完全可用。您可以玩游戏,并且所有基本功能都已到位。但是这个可玩的核心仍然缺少玩家在完成的游戏中期望或需要的总体功能。例如,现在您只能玩一次游戏;您需要退出并重新启动才能再次玩。让我们在屏幕上添加一个控件,以便玩家可以重新开始游戏而无需退出。
At this point, the Memory game is fully functional. You can play the game, and all the essential features are in place. But this playable core is still lacking the overarching functionality that players expect or need in a finished game. For example, right now you can play the game only once; you need to quit and restart to play again. Let’s add a control to the screen so that players can start the game over without having to quit.
此功能分为两个任务:创建一个 UI 按钮并在单击该按钮时重置游戏。图 5.12 显示了使用“开始”按钮时游戏的外观。
This functionality divides into two tasks: create a UI button and reset the game when that button is clicked. Figure 5.12 shows what the game will look like with the Start button.
Figure 5.12 Complete Memory game screen, including the Start button
顺便说一句,这两项任务都不是 2D 游戏所特有的。所有游戏都需要 UI 按钮,所有游戏都需要重置功能。我们将讨论这两个主题以完善本章。
Neither task is specific to 2D games, by the way. All games need UI buttons, and all games need the ability to reset. We’ll go over both topics to round out this chapter.
第一的,将按钮精灵从项目视图中向上拖动,将其放置在场景中。给它一个位置,如4.5、3.25、-10;这会将按钮放置在右上角(即向右 450 像素,向上 325 像素),并将其移近相机,以便它出现在其他游戏对象的顶部。因为我们希望能够点击这个对象,所以给它一个对撞机(就像卡片对象一样,选择添加组件 > Physics 2D > Box Collider 2D)。
First, place the button sprite in the scene by dragging it up from the Project view. Give it a position like 4.5, 3.25, -10; that will place the button in the top-right corner (that’s 450 pixels to the right and 325 pixels up) and move it nearer to the camera so that it’ll appear on top of other game objects. Because we want to be able to click this object, give it a collider (just as with the card object, choose Add Component > Physics 2D > Box Collider 2D).
注意如上一节所述,Unity 提供了多种创建 UI 显示的方法,包括在 Unity 后续版本中引入的高级 UI 系统。现在,我们将使用标准显示对象构建单个按钮。后续章节将教您有关高级 UI 功能的知识;2D 和 3D 游戏的 UI 最好使用该系统构建。
NOTE As alluded to in the previous section, Unity provides multiple ways to create UI displays, including an advanced UI system introduced in later versions of Unity. For now, we’ll build the single button out of standard display objects. A future chapter will teach you about the advanced UI functionality; the UI for both 2D and 3D games is ideally built with that system.
现在创建一个名为UIButton的新脚本并将其分配给按钮对象。
Now create a new script called UIButton and assign it to the button object.
Listing 5.13 Code to make a generic and reusable UI button
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 UIButton : MonoBehaviour {
[SerializeField] GameObject targetObject; ❶
[SerializeField] 字符串目标消息;
公共颜色突出显示颜色 = 颜色.青色;
公共无效OnMouseEnter(){
SpriteRenderer sprite = GetComponent<SpriteRenderer>();
如果(精灵!=空){
sprite.color = highlightColor; ❷
}
}
公共无效OnMouseExit(){
SpriteRenderer sprite = GetComponent<SpriteRenderer>();
如果(精灵!=空){
精灵.颜色 = 颜色.白色;
}
}
公共无效OnMouseDown(){
transform.localScale = new Vector3(1.1f, 1.1f, 1.1f); ❸
}
公共无效OnMouseUp(){
变换.localScale = Vector3.one;
如果 (targetObject != 空) {
目标对象.发送消息(目标消息); ❹
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UIButton : MonoBehaviour {
[SerializeField] GameObject targetObject; ❶
[SerializeField] string targetMessage;
public Color highlightColor = Color.cyan;
public void OnMouseEnter() {
SpriteRenderer sprite = GetComponent<SpriteRenderer>();
if (sprite != null) {
sprite.color = highlightColor; ❷
}
}
public void OnMouseExit() {
SpriteRenderer sprite = GetComponent<SpriteRenderer>();
if (sprite != null) {
sprite.color = Color.white;
}
}
public void OnMouseDown() {
transform.localScale = new Vector3(1.1f, 1.1f, 1.1f); ❸
}
public void OnMouseUp() {
transform.localScale = Vector3.one;
if (targetObject != null) {
targetObject.SendMessage(targetMessage); ❹
}
}
}
❶ Reference a target object to inform about clicks.
❷ Tint the button when the mouse hovers over it.
❸ The button’s size pops a bit when it’s clicked.
❹ Send a message to the target object when the button is clicked.
这段代码的大部分发生在一系列OnMouseSomething函数中。与Start()和Update()一样,这些是一系列自动可供 Unity 中的所有脚本组件使用的函数。MouseDown已在 5.2.2 节中提到,但如果对象具有对撞机,这些其他函数也会响应鼠标交互。MouseEnter和 MouseExit是一对用于将鼠标光标悬停在对象上的事件:MouseEnter在鼠标光标第一次移到对象上时触发,MouseExit在鼠标光标移开时触发。同样,MouseDown和MouseUp是一对用于单击鼠标的事件。MouseDown在物理按下鼠标按钮时触发,MouseUp在释放鼠标按钮时触发。
The majority of this code happens inside a series of OnMouseSomething functions. Like Start() and Update(), these are a series of functions automatically available to all script components in Unity. MouseDown was already mentioned in section 5.2.2, but these other functions also respond to mouse interactions if the object has a collider. MouseEnter and MouseExit are a pair of events used for hovering the mouse cursor over an object: MouseEnter triggers when the mouse cursor first moves over an object, and MouseExit triggers when the mouse cursor moves away. Similarly, MouseDown and MouseUp are a pair of events for clicking the mouse. MouseDown triggers when the mouse button is physically pressed, and MouseUp triggers when the mouse button is released.
您可以看到,当鼠标悬停在精灵上时,此代码会为精灵着色,当鼠标单击精灵时,会缩放精灵。在这两种情况下,您可以看到,当鼠标交互开始时,颜色或比例发生变化,然后当鼠标交互结束时,属性将返回默认值(白色或比例 1)。对于缩放,代码使用所有 GameObject 都具有的标准变换组件。但对于着色,代码使用精灵对象具有的SpriteRenderer组件;精灵通过公共变量设置为 Unity 编辑器中定义的颜色。
You can see that this code tints the sprite when the mouse hovers over it and scales the sprite when it’s clicked. In both cases, you can see that the change (in color or scale) happens when the mouse interaction begins, and then the property returns to the default (either white or scale 1) when the mouse interaction ends. For scaling, the code uses the standard transform component that all GameObjects have. For tint, though, the code uses the SpriteRenderer component that sprite objects have; the sprite is set to a color that’s defined in Unity’s editor through a public variable.
除了将比例返回到 1 之外,释放鼠标时还会调用SendMessage() 。 SendMessage()会在该 GameObject 的所有组件中调用给定名称的函数。这里,消息的目标对象以及要发送的消息均由序列化变量定义。这样,同一个UIButton组件可以用于各种按钮,不同按钮的目标在 Inspector 中设置为不同的对象。
In addition to returning the scale to 1, SendMessage() is called when the mouse is released. SendMessage() calls the function of the given name in all components of that GameObject. Here, the target object for the message, as well as the message to send, are both defined by serialized variables. This way, the same UIButton component can be used for all sorts of buttons, with the target of different buttons set to different objects in the Inspector.
通常,在使用 C# 等强类型语言进行 OOP 时,您需要知道目标对象的类型才能与该对象进行通信(例如,调用对象的公共方法,如调用targetObject.SendMessage()本身)。但 UI 元素的脚本可能有很多类型的目标,因此 Unity 提供了SendMessage()方法即使您不知道目标对象的具体类型,也可以与目标对象传达特定消息。
Normally, when doing OOP in a strongly typed language like C#, you need to know the type of a target object in order to communicate with that object (for example, to call a public method of the object, like calling targetObject.SendMessage() itself). But scripts for UI elements may have lots of types of targets, so Unity provides the SendMessage() method to communicate specific messages with a target object even if you don’t know exactly what type of object it is.
警告:使用SendMessage()对 CPU 的效率低于调用已知类型的公共方法(即使用object.SendMessage("Method")而不是component.Method()),因此,只有当SendMessage()可以让代码更易于理解和使用时,才使用 SendMessage()。一般来说,只有当许多不同类型的对象可以接收消息时,才会出现这种情况。在这种情况下,继承甚至接口的不灵活性会阻碍游戏开发过程并阻碍实验。
WARNING Using SendMessage() is less efficient for the CPU than calling public methods on known types (that is, using object.SendMessage("Method") versus component.Method()), so use SendMessage() only when it’s a big win in terms of making the code simpler to understand and work with. As a general rule, that will be the case only if lots of different types of objects could be receiving the message. In situations like that, the inflexibility of inheritance or even interfaces will hinder the game development process and discourage experimentation.
写好代码后,在按钮的 Inspector 中连接公共变量。高亮颜色可以设置为任何你喜欢的颜色(虽然默认的青色在蓝色按钮上看起来很不错)。同时,将SceneController对象在目标对象槽中,然后输入Restart作为消息。
With this code written, wire up the public variables in the button’s Inspector. The highlight color can be set to whatever you’d like (although the default cyan looks pretty good on a blue button). Meanwhile, put the SceneController object in the target object slot, and then type Restart as the message.
如果你现在玩游戏,右上角的“重置”按钮会根据鼠标改变颜色,并在单击时产生轻微的视觉效果。但是,单击按钮时会发出一条错误消息;在控制台中,你会看到一条错误消息,提示没有接收“重启”消息。这是因为我们还没有编写 Restart ()方法在SceneController中,让我们添加那。
If you play the game now, the Reset button in the top-right corner changes color in response to the mouse and makes a slight visual pop when clicked. But an error message will be emitted when you click the button; in the console, you’ll see an error about there not being a receiver for the Restart message. That’s because we haven’t written a Restart() method in SceneController, so let’s add that.
这按钮中的SendMessage()方法尝试调用SceneController中的Restart(),所以我们现在添加它。
The SendMessage() method from the button attempts to call Restart() in the SceneController, so let’s add that now.
清单 5.14重新加载关卡的SceneController代码
Listing 5.14 SceneController code that reloads the level
... 使用 UnityEngine.SceneManagement; ❶ ... 公共无效重新启动(){ SceneManager.LoadScene("场景"); ❷ } ...
... using UnityEngine.SceneManagement; ❶ ... public void Restart() { SceneManager.LoadScene("Scene"); ❷ } ...
❶ Include SceneManagement code.
❷ If your scene has a different name, change the name in this string.
您可以看到Restart()所做的一件事是调用LoadScene()。该方法加载已保存的场景资产(在 Unity 中单击“保存场景”时创建的文件)。将要加载的场景的名称传递给该方法。在我的例子中,场景以名称Scene保存,但如果您使用了其他名称,请将其传递给该方法。
You can see the one thing Restart() does is call LoadScene(). That method loads a saved scene asset (the file created when you click Save Scene in Unity). Pass the name of the scene you want to load into the method. In my case, the scene was saved with the name Scene, but if you used a different name, pass that to the method instead.
单击“播放”查看会发生什么。显示几张卡片并进行几场匹配。如果您随后单击“重置”按钮,游戏将重新开始,所有卡片均隐藏,分数为 0。太棒了,这正是我们想要的!
Click Play to see what happens. Reveal a few cards and make a few matches. If you then click the Reset button, the game starts over, with all cards hidden and a score of 0. Great, just what we wanted!
正如名称LoadScene()所示,此方法可以加载不同的场景。但是,当场景加载时究竟会发生什么?为什么这会重置游戏?发生的事情是,当前级别的所有内容(场景中的所有对象,以及附加到这些对象的所有脚本)都会从内存中清除,然后加载新场景中的所有内容。因为新场景是当前场景的已保存资产(在本例中),所以所有内容都会从内存中清除,然后从头开始重新加载。
As the name LoadScene() indicates, this method can load different scenes. But what exactly happens when a scene loads, and why does this reset the game? What happens is that everything from the current level (all objects in the scene, and thus all scripts attached to those objects) is flushed from memory, and then everything from the new scene is loaded. Because the new scene is the saved asset of the current scene (in this case), everything is flushed from memory and then reloaded from scratch.
提示:您可以标记特定对象,使其在加载关卡时不被默认内存刷新。Unity 提供了DontDestroyOnLoad()方法让一个对象在多个场景中存在。您将在后面的章节中对代码架构的部分使用此方法。
TIP You can mark specific objects to exclude from the default memory flush when a level is loaded. Unity provides the DontDestroyOnLoad() method to keep an object around in multiple scenes. You’ll use this method on parts of the code architecture in later chapters.
又一款游戏成功完成!好吧,完成是一个相对术语;你总是可以实现更多功能,但初始计划中的一切都已完成。这款 2D 游戏中的许多概念也适用于 3D 游戏,尤其是检查游戏状态和加载级别。是时候再次换个方向,离开这款记忆游戏,转向新的项目。
Another game successfully completed! Well, completed is a relative term; you could always implement more features, but everything from the initial plan is done. Many of the concepts from this 2D game apply to 3D games as well, especially the checking of game state and loading levels. It’s time to switch gears yet again and move away from this Memory game and on to new projects.
让我们创建一个新游戏并继续学习 Unity 的 2D 功能。第 5 章介绍了基本概念,因此本章将在此基础上创建更复杂的游戏。具体来说,您将构建 2D 平台游戏的核心功能。这种常见的 2D 动作游戏也称为平台游戏,以《超级马里奥兄弟》等经典游戏而闻名:从侧面看,角色在平台上奔跑和跳跃,视图滚动跟随。图 6.1 显示了最终结果。
Let’s create a new game and continue learning about Unity’s 2D functionality. Chapter 5 covered the fundamental concepts, so this chapter builds on those to create a more elaborate game. Specifically, you are going to build the core functionality of a 2D platform game. Also called a platformer, this common type of 2D action game is best known for classics like Super Mario Brothers: a character viewed from the side runs and jumps on platforms, and the view scrolls around to follow. Figure 6.1 shows what the end result will be.
Figure 6.1 The final product of this chapter
这个项目将教授诸如左右移动玩家、播放精灵的动画以及添加跳跃能力等概念。我们还将介绍平台游戏中常见的几个特殊功能,例如单向地板和移动平台。从这个外壳到完整的游戏,主要意味着一遍又一遍地重复这些概念。
This project will teach concepts like moving the player left and right, playing the sprite’s animation, and adding the ability to jump. We’ll also go over several special features common in platform games, like one-way floors and moving platforms. Going from this shell to a full game mostly means repeating those concepts over and over.
首先,像上一章一样在 2D 模式下创建一个新项目:从 Unity Hub 中选择“新建”,或从“文件”菜单中选择“新建项目”;然后在出现的窗口中选择“2D”。在新项目中,创建两个文件夹,分别名为Sprites和Scripts,以包含各种资产。您可以像第 5 章一样调整相机,但现在只需将尺寸减小到4。这个项目不需要完美的相机设置,但您需要调整尺寸以适应准备发布的精致游戏。
To get started, create a new project in 2D mode as in the last chapter: from Unity Hub, choose New, or from the File menu choose New Project; then select 2D in the window that appears. In the new project, create two folders, called Sprites and Scripts, to contain the various assets. You could adjust the camera as in chapter 5, but for now just reduce Size to 4. This project doesn’t require a perfect camera setup, although you would need to adjust the size for a polished game that’s ready for release.
提示屏幕中央的相机图标可能会碍事,因此您可以使用 Gizmos 菜单将其隐藏。Scene 视图顶部有一个 Gizmos 标签。该术语指的是编辑器中的抽象形状和图标。单击 Gizmos 查看按字母顺序排列的列表,然后单击相机旁边的图标。
TIP The camera icon in the center of the screen can get in the way, so you can hide it by using the Gizmos menu. Along the top of the Scene view is a label for Gizmos. That term refers to the abstract shapes and icons in the editor. Click Gizmos for an alphabetical list and then click the icon next to Camera.
现在保存空场景(当然,在工作时要定期单击“保存”)以在此项目中创建场景资产。目前一切都是空的,因此第一步是引入艺术资产。
Now save the empty scene (and of course click Save periodically while you work) to create the Scene asset in this project. Everything is empty at the moment, so the first step will be bringing in art assets.
前如果你想编写 2D 平台游戏的功能,你需要将图像导入项目(请记住,2D 游戏中的图像称为精灵而不是纹理),然后将这些精灵放入场景中。此游戏将是 2D 平台游戏的外壳,玩家控制的角色在一个基本且几乎空白的场景中奔跑,因此你只需要为平台和玩家创建几个精灵。让我们分别介绍一下,因为尽管此示例中的图像很简单,但其中涉及一些不太明显的注意事项。
Before you can program the functionality of a 2D platform game, you need to import images into the project (remember, images in a 2D game are referred to as sprites instead of textures) and then place those sprites into the scene. This game will be the shell of a 2D platform game, with a player-controlled character running around a basic and mostly empty scene, so all you need are a couple of sprites for the platforms and for the player. Let’s go over each separately, because although the images in this example are simple, some nonobvious considerations are involved.
简单地换句话说,您需要一张空白的白色图像来使用。本章的示例项目中包含一个名为 blank.png 的图像;下载示例项目并从中复制 blank.png。然后将 PNG 拖到新项目的 Sprites 文件夹中,并确保在 Inspector 中导入设置指示它是 Sprite 而不是纹理(对于 2D 项目,这应该是自动的,但值得仔细检查)。
Simply put, you need a single blank white image to use here. An image called blank.png is included in the sample project for this chapter; download the sample project and copy blank.png from there. Then drag the PNG into the Sprites folder of your new project, and make sure in the Inspector that Import Settings indicate it’s a Sprite rather than a Texture (that should be automatic for a 2D project, but it’s worth double-checking).
您现在所做的与第 4 章中的白盒制作基本相同,但采用的是 2D 而非 3D。2D 中的白盒制作使用精灵而非网格完成,但保留了相同的活动,即阻挡空白地板和墙壁以供玩家四处移动。
What you’re doing now is essentially the same as the whiteboxing from chapter 4, but in 2D instead of 3D. Whiteboxing in 2D is done with sprites rather than meshes but maintains the same activity of blocking out blank floors and walls for the player to move around.
要放置地板对象,请将空白精灵拖入场景中(如图 6.2 所示)(位置0.15、-1.27、0左右),将比例设置为50、2、1,并将其名称更改为Floor。然后拖入另一个空白精灵,将其比例设置为6、6、1,将其放置在右侧的地板上(位置2、-0.63、0左右),并将其命名为Block
To place the floor object, drag the blank sprite into the scene as shown in figure 6.2 (around Position 0.15, -1.27, 0), set Scale to 50, 2, 1, and change its name to Floor. Then drag in another blank sprite, set its Scale to 6, 6, 1, place it on the floor off to the right (around Position 2, -0.63, 0), and name it Block
Figure 6.2 Floor platform placement
很简单;现在地板和方块已经完成了。你需要的另一个对象是播放器。
Simple enough; now the floor and block are done. The other object you need is a character for the player.
这您唯一需要的其他艺术资产是玩家的精灵,因此也从示例项目中复制 stickman.png。但与空白图像不同,此 PNG 是一系列单独的精灵组合成一张图像。如图 6.3 所示,火柴人图像是两个动画的帧:静止不动和行走循环。
The only other art asset you need is the player’s sprite, so also copy stickman.png from the sample project. But unlike the blank image, this PNG is a series of separate sprites assembled into one image. As shown in figure 6.3, the stickman image is the frames of two animations: standing idle and a walk cycle.
Figure 6.3 Stickman sprite sheet—six frames in a row
我们不会详细介绍如何制作动画,但可以说空闲和循环都是游戏开发者常用的术语。空闲是指不做任何事时的细微动作,循环是指连续循环的动画。
We’re not going into detail on how to animate, but suffice to say that idle and cycle are both common terms used by game developers. Idle refers to subtle movement while doing nothing, and cycle is an animation that loops continuously.
如第 5 章所述,图像文件可能是一堆打包在一起的精灵图像,而不仅仅是单个精灵。当多个精灵图像是动画的帧时,这样的图像称为精灵表。在 Unity 中,作为多个精灵导入的图像仍将作为单个资产出现在项目视图中,但如果您单击资产上的箭头,它将展开并显示所有单个精灵图像。图 6.4 显示了它的外观。
As explained in chapter 5, an image file may be a bunch of sprite images packed together, rather than just a single sprite. Images like this are called sprite sheets when the multiple sprite images are frames of an animation. In Unity, an image imported as multiple sprites will still appear in the Project view as a single asset, but if you click the arrow on the asset, it’ll expand and show all the individual sprite images. Figure 6.4 shows how that looks.
Figure 6.4 Slicing a sprite sheet into separate frames
将 stickman.png 拖到 Sprites 文件夹中以导入图像,但这次需要在 Inspector 中更改许多导入设置。选择精灵资产,将 Sprite Mode 设置为 Multiple,然后单击 Sprite Editor 打开该窗口。单击窗口左上角的 Slice,将 Type 设置为 Grid By Cell Size(如图 6.4 所示),使用 Size 32 , 64(这是精灵表中每个帧的大小),然后单击 Slice 以查看分割的帧。现在关闭 Sprite Editor 窗口并单击 Apply 以保存更改。
Drag stickman.png into the Sprites folder to import the image, but this time change a lot of Import Settings in the Inspector. Select the sprite asset, set Sprite Mode to Multiple, and then click Sprite Editor to open that window. Click Slice at the top left of the window, set Type to Grid By Cell Size (shown in figure 6.4), use Size 32, 64 (this is the size of each frame in the sprite sheet), and click Slice to see the frames split up. Now close the Sprite Editor window and click Apply to keep the changes.
注意Sprite Editor 窗口需要 2D Sprite 包。创建新的 2D 项目应该会自动安装该包,但如果没有,请打开 Window > Package Manager 并在窗口左侧的列表中查找 2D Sprite。选择该包,然后单击 Install 按钮。
NOTE The Sprite Editor window requires the 2D Sprite package. Creating a new 2D project should have automatically installed that package, but if not, then open Window > Package Manager and look for 2D Sprite in the list on the left side of the window. Select that package and then click the Install button.
警告:如果窗口太小,Sprite Editor 窗口顶部的按钮会被隐藏。如果您没有看到“切片”按钮,请尝试拖动窗口的一角来调整其大小。
WARNING The buttons on top of the Sprite Editor window get hidden if the window is too small. If you don’t see the Slice button, try dragging the corner of the window to resize it.
精灵资源现在已拆分,因此单击箭头以展开帧。将一个(可能是第一个)火柴人精灵拖到场景中,将其放置在地板中间,并将其命名为Player。在那里,玩家对象位于这场景!
The sprite asset is now split up, so click the arrow to expand the frames. Drag one (probably the first) stickman sprite into the scene, place it standing on the middle of the floor, and name it Player. There, the player object is in the scene!
现在图形设置完成后,让我们开始对玩家的移动进行编程。首先,场景中的玩家实体需要几个额外的组件供我们控制。如前几章简要提到的,Unity 中的物理模拟通过特殊的 Rigidbody 组件作用于对象,而您希望物理(特别是碰撞和重力)作用于角色。
Now that the graphics are set up, let’s start programming the player’s movement. First off, the player entity in the scene needs a couple of additional components for us to control. As mentioned briefly in previous chapters, the physics simulation in Unity acts on objects with the special Rigidbody component, and you want physics (collisions and gravity in particular) to act on the character.
同时,角色还需要一个 Collider 组件来定义其边界以进行碰撞检测。这两个组件之间的区别很微妙但很重要:Collider 定义物理作用的形状,而 Rigidbody 告诉物理模拟作用于哪些对象。
Meanwhile, the character also needs a Collider component to define its boundaries for collision detection. The difference between these components is subtle but important: the Collider defines the shape for physics to act on, and the Rigidbody tells the physics simulation what objects to act on.
注意这些组件是分开的(即使它们密切相关),因为许多本身不需要物理模拟的物体确实需要与受物理作用的其他物体发生碰撞。
NOTE These components are kept separate (even though they are closely related) because many objects that don’t need the physics simulation themselves do need to collide with other objects that are acted on by physics.
需要注意的另一个细节是,Unity 为 2D 游戏提供了单独的物理系统,而不是 3D 物理系统。因此,在本章中,您将使用列表中“2D 物理”部分的组件,而不是常规“物理”部分的组件。
One other subtlety to be aware of is that Unity has a separate physics system for 2D games instead of 3D physics. Thus, in this chapter you’ll be using components from the Physics 2D section instead of the regular Physics section of the list.
在场景中选择 Player。在 Inspector 中,单击 Add Component,然后选择 Physics 2D > Rigidbody 2D,如图 6.5 所示。然后再次单击 Add Component 以添加 Physics 2D > Box Collider 2D。Rigidbody 需要少量微调,因此在 Inspector 中将 Collision Detection 设置为 Continuous,打开 Constraints > Freeze Rotation Z(通常,物理模拟会在移动物体时尝试旋转物体,但游戏中的角色不会像普通物体那样表现),并将 Gravity Scale 降低为0(稍后您将重置它,但现在您不需要重力)。玩家实体现在已准备好用于控制移动的脚本。
Select Player in the scene. In the Inspector, click Add Component and then choose Physics 2D > Rigidbody 2D, as shown in figure 6.5. Then click Add Component again to add Physics 2D > Box Collider 2D. The Rigidbody needs a small amount of fine-tuning, so in the Inspector set Collision Detection as Continuous, turn on Constraints > Freeze Rotation Z (normally, the physics simulation will attempt to rotate objects while moving them, but characters in games don’t behave like normal objects), and reduce Gravity Scale to 0 (you’ll reset this later, but for now you don’t want gravity). The player entity is now ready for the script that controls movement.
Figure 6.5 Add and adjust the Rigidbody 2D component
到开始,您将让玩家左右移动;垂直移动在平台游戏中也很重要,但您稍后会处理它。在 Scripts 文件夹中创建一个名为PlatformerPlayer 的C# 脚本,然后将其拖到场景中的 Player 对象上。打开脚本并根据此清单编写代码。
To begin, you’ll make the player move left and right; vertical movement is important also in a platformer, but you’ll deal with that later. Create a C# script called PlatformerPlayer in the Scripts folder, and then drag that onto the Player object in the scene. Open the script and write the code from this listing.
清单 6.1 PlatformerPlayer脚本使用箭头键移动
Listing 6.1 PlatformerPlayer script to move with arrow keys
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 PlatformerPlayer : MonoBehaviour {
公共浮动速度=4.5f;
私人 Rigidbody2D 身体;
无效开始(){
body = GetComponent<Rigidbody2D>(); ❶
}
无效更新(){
float deltaX = Input.GetAxis("水平") * 速度;
Vector2 运动 = new Vector2(deltaX, body.velocity.y); ❷
身体.速度 = 运动;
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlatformerPlayer : MonoBehaviour {
public float speed = 4.5f;
private Rigidbody2D body;
void Start() {
body = GetComponent<Rigidbody2D>(); ❶
}
void Update() {
float deltaX = Input.GetAxis("Horizontal") * speed;
Vector2 movement = new Vector2(deltaX, body.velocity.y); ❷
body.velocity = movement;
}
}
❶ Need this other component attached to this GameObject
❷ Set only horizontal movement; preserve preexisting vertical movement.
编写代码后,单击“播放”,然后可以使用箭头键移动玩家。该代码与前几章中的移动代码非常相似,主要区别在于它作用于Rigidbody2D而不是CharacterController。CharacterController用于 3D,因此对于 2D 游戏,您可以使用Rigidbody组件。请注意,运动应用于Rigidbody的速度,而不是位置之类的。
After writing the code, click Play and you can move the player by using the arrow keys. The code is fairly similar to movement code in previous chapters, with the main difference being that it acts on Rigidbody2D instead of CharacterController. CharacterController is for 3D, so for a 2D game you use a Rigidbody component. Note that the movement is applied to Rigidbody’s velocity, rather than something like position.
注意:此代码不需要使用增量时间。在之前的章节中,我们需要考虑帧之间的时间以实现与帧速率无关的移动,但在本章中我们不需要这样做。在这里,我们调整的是速度,这本质上与帧速率无关,而不是位置。在之前的章节中,我们直接调整位置。
NOTE This code doesn’t need to use delta time. In previous chapters, we needed to factor in the time between frames to achieve frame rate-independent movement, but we don’t need to do that in this chapter. Here, we are adjusting velocity, which is inherently frame-rate independent, rather than position. In previous chapters, we were adjusting position directly.
提示默认情况下,Unity 会对箭头键输入应用一点加速度。不过,对于平台游戏来说,这可能会让人感觉迟钝。为了获得更敏捷的控制,请将水平输入的灵敏度和重力增加到6。要找到这些设置,请选择编辑 > 项目设置 > 输入管理器;您将看到一个长列表,但水平是第一部分。
TIP By default, Unity applies a bit of acceleration to arrow key input. That can feel sluggish for a platformer, though. For snappier control, increase Sensitivity and Gravity of Horizontal input to 6. To find those settings, choose Edit > Project Settings > Input Manager; you’ll see a long list, but Horizontal is the first section.
太棒了——这个项目已经完成了大部分水平移动!你只需要处理碰撞检测。
Great—this project is most of the way there for horizontal movement! You need to address only collision detection.
作为您可能已经注意到,玩家现在穿过了方块。地板或方块上没有碰撞器,因此玩家可以穿过它们。要解决此问题,请将 Box Collider 2D 添加到 Floor 和 Block:选择场景中的每个对象,单击 Inspector 中的 Add Component,然后选择 Physics 2D > Box Collider 2D。
As you’ve probably noticed, the player walks through the block right now. There are no colliders on the floor or block, so the player can move through them. To fix this, add Box Collider 2D to Floor and Block: select each object in the scene, click Add Component in the Inspector, and choose Physics 2D > Box Collider 2D.
这就是您需要做的全部!现在单击“播放”,玩家将无法穿过方块。与第 2 章中移动玩家一样,如果您直接调整玩家的位置,碰撞检测将不起作用。但是,如果您将移动应用于玩家的物理组件,Unity 的内置碰撞检测就可以起作用。换句话说,移动Transform .position会忽略碰撞检测,因此您可以在移动脚本中操纵Rigidbody2D .velocity。
And that’s all you need to do! Click Play now, and the player won’t be able to move through the block. As with moving the player in chapter 2, if you had adjusted the player’s position directly, collision detection wouldn’t work. But Unity’s built-in collision detection can work if you apply the movement to the player’s physics components. In other words, moving Transform .position would have ignored collision detection, so instead you manipulated Rigidbody2D .velocity in the movement script.
为更复杂的艺术品添加碰撞器可能会稍微棘手一些,但坦率地说,在这种情况下也不会太难。即使艺术品不完全是矩形,您可能仍想使用盒子碰撞器并大致围绕场景中障碍物的形状。或者,您可以尝试其他碰撞器形状,包括任意自定义多边形形状。图 6.6 说明了如何使用多边形碰撞器来处理形状奇特的物体。
Adding colliders to more complex art could be slightly trickier, but frankly not much harder in that case. Even if the art is not exactly a rectangle, you may still want to use box colliders and roughly surround the shape of obstacles in the scene. Alternatively, you could try other collider shapes, including arbitrary custom polygon shapes. Figure 6.6 illustrates how to work with polygon colliders for oddly shaped objects.
Figure 6.6 Edit the shape of the polygon collider with the Edit Collider button.
无论如何,碰撞检测现在正在工作,所以下一步是让玩家和它是移动。
Anyway, collision detection is now working, so the next step is making the player animate along with its movement.
什么时候导入 stickman.png 后,将其拆分为多个帧以进行动画处理。现在让我们播放该动画,这样玩家就不是在滑动,而是看起来像是在行走。
When stickman.png was imported, it was split into multiple frames for animating. Now let’s play that animation, so that the player isn’t sliding around but appears to be walking.
作为在第 4 章中简要提到过,Unity 中的动画系统称为Mecanim。它旨在让您能够直观地为角色设置复杂的动画网络,然后使用最少的代码控制这些动画。该系统最适合 3D 角色(因此,我们将在后续章节中更详细地介绍它),但对于 2D 角色也同样有用。
As mentioned briefly in chapter 4, the animation system in Unity is called Mecanim. It’s designed so that you can visually set up a complex network of animations for a character and then control those animations with a minimum of code. The system is most useful for 3D characters (thus, we cover it in more detail in future chapters) but is still useful for 2D characters too.
动画系统的核心由两种资产组成:动画剪辑和动画控制器。注意动画与动画器:剪辑是要播放的单独动画循环,而控制器是控制何时播放动画的网络。这个网络是一个状态机图,图中的状态是可以播放的不同动画。控制器根据所观察的条件在状态之间切换,并在每个状态下播放不同的动画。
The heart of the animation system is composed of two kinds of assets: animation clips and animator controllers. Notice animation versus animator : clips are the individual animation loops to play, whereas the controller is the network controlling when to play animations. This network is a state machine diagram, and the states in the diagram are different animations that could be playing. The controller shifts between states in reaction to conditions it is watching, and plays a different animation in each state.
当您将 2D 动画拖入场景时,Unity 会自动创建这两种资源。也就是说,当您将动画的帧拖入场景时,Unity 会自动使用这些帧创建动画剪辑和动画控制器。如图 6.7 所示,展开精灵资源的所有帧,选择 0-1 帧,将它们拖入场景,然后在确认窗口中输入名称stickman_idle 。
Unity will create both kinds of assets automatically when you drag a 2D animation into the scene. That is, when you drag the frames of an animation into the scene, Unity will automatically create an animation clip and an animator controller using those frames. As depicted in figure 6.7, expand all the frames of the sprite asset, select frames 0-1, drag them into the scene, and type the name stickman_idle in the confirmation window.
Figure 6.7 Steps to use sprite-sheet frames in an Animator component
将帧拖入“场景”视图的操作会在“资产”视图中创建两个内容:一个名为stickman_idle的剪辑和一个名为stickman_0 的控制器。此操作还会在场景中创建一个名为stickman_0的对象,但您不需要它,因此请将其删除。将控制器重命名为不带后缀的stickman。太棒了 - 您创建了角色的空闲动画!
The action of dragging frames into the Scene view creates two things in the Asset view: a clip named stickman_idle and a controller named stickman_0. This action also creates an object called stickman_0 in the scene, but you don’t need that, so delete it. Rename the controller stickman with no suffix. Great—you created the character’s idle animation!
现在重复此过程以制作行走动画。选择第 2-5 帧,将它们拖到场景中,并将动画命名为stickman_walk。这次,删除场景中的stickman_2和 Assets 中的新控制器;只需要一个动画控制器来控制两个动画剪辑,因此保留旧的并删除新创建的stickman_2 。
Now repeat the process for the walk animation. Select frames 2-5, drag them into the scene, and name the animation stickman_walk. This time, delete both stickman_2 in the scene and the new controller in Assets; only one animator controller is needed to control both animation clips, so keep the old one and delete stickman_2, the newly created one.
要将控制器应用于玩家角色,请在场景中选择 Player,然后单击“添加组件”以选择“杂项”>“动画器”。如图 6.7 所示,将火柴人控制器拖到 Inspector 中的控制器插槽中。在仍选择 Player 的情况下,打开“窗口”>“动画”>“动画器”(如图 6.8 所示)。“动画器”窗口中的动画显示为块,称为状态,并且控制器在运行时在状态之间切换。这个特定的控制器已经有空闲状态,但您需要添加行走状态;将stickman_walk动画剪辑从 Assets 拖到 Animator 窗口中。
To apply the controller to your player character, select Player in the scene and click Add Component to choose Miscellaneous > Animator. As shown in figure 6.7, drag the stickman controller into the controller slot in the Inspector. With the Player still selected, open Window > Animation > Animator (shown in figure 6.8). Animations in the Animator window are displayed as blocks, referred to as states, and the controller switches between states when running. This particular controller already has the idle state in it, but you need to add a walking state; drag the stickman_walk animation clip from Assets into the Animator window.
默认情况下,空闲动画播放速度太快。要降低空闲速度,请选择空闲动画状态,然后在右侧面板中将速度设置设置为0.2。进行此更改后,所有动画都已为下一个设置步。
By default, the idle animation will play too fast. To decrease the idle speed, select the idle animation state, and in the right-hand panel set the Speed setting to 0.2. With that change, the animations are all set up for the next step.
现在在动画控制器中设置动画状态后,您可以在这些状态之间切换以播放不同的动画。如上一节所述,状态机根据其正在观察的条件切换状态。在 Unity 的动画控制器中,这些条件称为参数,所以我们来添加一个。图 6.8 指出了相关控件:选择“参数”选项卡,然后单击“+”按钮以显示参数类型菜单。添加一个名为speed的浮点参数。
Now that you’ve set up animation states in the animator controller, you can switch between those states to play the different animations. As mentioned in the preceding section, a state machine switches states in reaction to conditions it is watching. In Unity’s animation controllers, those conditions are referred to as parameters, so let’s add one. Figure 6.8 pointed out the relevant controls: select the Parameters tab and click the + button for a menu of parameter types. Add a float parameter called speed.
Figure 6.8 Animator window, showing animation states and transitions
接下来,您需要根据该参数在动画状态之间切换。右键单击stickman_idle并选择 Make Transition;这将开始从空闲状态拖出一个箭头。单击stickman_walk以连接到该状态,并且由于转换是单向的,因此右键单击stickman_walk以转换回来。
Next, you need to switch between animation states based on that parameter. Right-click stickman_idle and select Make Transition; that’ll start dragging out an arrow from the idle state. Click stickman_walk to connect to that state, and because transitions are unidirectional, also right-click stickman_walk to transition back.
现在选择从空闲状态开始的转换(您可以单击箭头本身),取消选中“有退出时间”,然后单击底部的 + 以添加条件(再次如图 6.8 所示)。使条件速度大于(大于)0.1,这样状态将在该条件下转换。现在再次执行从行走到空闲的转换:选择从行走状态开始的转换,取消选中“有退出时间”,添加条件,并使条件速度小于(小于)0.1。
Now select the transition from idle (you can click the arrows themselves), uncheck Has Exit Time, and click the + at the bottom to add a condition (again, shown in figure 6.8). Make the condition speed Greater (than) 0.1 so the states will transition in that condition. Now do it again for the walk-to-idle transition: select the transition from walk, uncheck Has Exit Time, add a condition, and make the condition speed Less (than) 0.1.
最后,PlatformerPlayer脚本可以操纵动画控制器,如本清单所示。
Finally, the PlatformerPlayer script can manipulate the animator controller, as shown in this listing.
Listing 6.2 Triggering animations along with moving
...
私人动画师动画;
...
无效开始(){
body = GetComponent<Rigidbody2D>(); ❶
动画 = GetComponent<Animator>();
}
无效更新(){
...
anim.SetFloat("速度", Mathf.Abs(deltaX)); ❷
if (!Mathf.Approximately(deltaX, 0)) { ❸
transform.localScale = new Vector3(Mathf.Sign(deltaX), 1, 1); ❹
}
}
......
private Animator anim;
...
void Start() {
body = GetComponent<Rigidbody2D>(); ❶
anim = GetComponent<Animator>();
}
void Update() {
...
anim.SetFloat("speed", Mathf.Abs(deltaX)); ❷
if (!Mathf.Approximately(deltaX, 0)) { ❸
transform.localScale = new Vector3(Mathf.Sign(deltaX), 1, 1); ❹
}
}
...
❶ Existing code to help show where to position new code
❷ Speed is greater than zero even if velocity is negative.
❸浮点数并不总是精确的,因此请使用 Approximately() 进行比较。
❸ Floats aren’t always exact, so compare using Approximately().
❹ When moving, scale positive or negative 1 to face right or left.
哇,几乎没有任何控制动画的代码!大部分工作由 Mecanim 处理,只需要少量代码即可操作动画。玩游戏并四处移动以观看玩家精灵的动画。这款游戏真的很不错,所以继续下一个步!
Wow, that was barely any code for controlling the animations! Most of the work is handled by Mecanim, and only a small amount of code is needed to operate the animations. Play the game and move around to watch the player sprite animate. This game is really coming along, so on to the next step!
这玩家可以前后移动,但还不能垂直移动。垂直移动(从壁架上掉下来和跳到更高的平台)是平台游戏的重要组成部分,所以接下来让我们实现它。
The player can move back and forth but isn’t yet moving vertically. Vertical movement (both falling off ledges and jumping to higher platforms) is an important part of platform games, so let’s implement that next.
有些与直觉相反,在让玩家跳跃之前,玩家需要借助重力才能跳跃。您可能还记得,之前您将玩家刚体上的重力比例设置为0。这样玩家就不会因为重力而跌落。那么,现在将其恢复为1:选择场景中的玩家对象,在检查器中找到刚体,然后在重力比例中输入1。
Somewhat counterintuitively, before you can make the player jump, it needs gravity to jump against. As you may recall, earlier you set Gravity Scale to 0 on the player’s Rigidbody. That was so the player wouldn’t fall because of gravity. Well, turn that back to 1 now: select the Player object in the scene, find Rigidbody in the Inspector, and then type 1 in Gravity Scale.
重力现在影响着玩家,但是(假设您已将 Box Collider 添加到 Floor 对象)地板会将他们托起。从地板两侧走开,就会坠入虚空。默认情况下,重力对玩家的影响较弱,因此您需要增加其影响的幅度。物理模拟包括一个全局重力设置,您可以在“编辑”菜单中进行调整。具体来说,选择“编辑”>“项目设置”>“物理 2D”。如图 6.9 所示,在各种控件和设置的顶部,您应该看到“重力 Y”;将其更改为-40。
Gravity is now affecting the player, but (assuming you had added a Box Collider to the Floor object) the floor is holding them up. Walk off the sides of the floor to fall into oblivion. By default, gravity affects the player somewhat weakly, so you’ll want to increase the magnitude of its effect. The physics simulation includes a global gravity setting, which you can adjust in the Edit menu. Specifically, choose Edit > Project Settings > Physics 2D. As shown in figure 6.9, at the top of the various controls and settings, you should see Gravity Y; change that to -40.
Figure 6.9 Gravity intensity in Physics settings
您可能已经注意到一个微妙的问题:坠落的玩家会粘在地板的一侧。要看到这个问题,请从平台边缘走开,然后立即反向向平台移动。呃,不好!幸运的是,Unity 可以轻松解决这个问题。只需将 Physics 2D > Platform Effector 2D 组件添加到 Block 和 Floor 即可。此效应器使场景中的对象表现得更像平台游戏中的平台。图 6.10 指出了两个需要调整的设置:在对撞机上设置 Used By Effector,并在效应器上关闭 Use One Way(我们将在其他平台上使用后一种设置,但现在不这样做)。
You may have noticed one subtle issue: the falling player sticks to the side of the floor. To see this problem, walk off the edge of the platform and immediately reverse direction to move back toward the platform. Ugh, not good! Fortunately, Unity makes that easy to fix. Just add the Physics 2D > Platform Effector 2D components to Block and Floor. This effector makes objects in the scene behave more like platforms in a platform game. Figure 6.10 points out two settings to adjust: Set Used By Effector on the collider, and turn off Use One Way on the effector (we’ll use this latter setting for other platforms, but not now).
Figure 6.10 Collider and effector settings in the Inspector
这解决了垂直运动的向下部分,但你仍然需要处理向上的部分部分。
That takes care of the downward part of vertical movement, but you still need to take care of the upward part.
这您需要的下一个动作是跳跃。当玩家点击跳跃按钮时(我们将使用空格键),会施加向上的冲击。虽然您的代码直接改变了水平移动的速度,但您将保留垂直速度,以便重力可以发挥作用。相反,物体可能会受到重力以外的其他力的影响,因此您将添加向上的力。将此代码添加到PlatformerPlayer脚本中。
The next action you need is jumping. That is an upward jolt applied when the player clicks the Jump button (we’ll use the spacebar). Although your code directly changed the velocity for horizontal movement, you’re going to leave vertical velocity alone so gravity can do its work. Instead, objects can be influenced by other forces besides gravity, so you’ll add an upward force. Add this code to the PlatformerPlayer script.
Listing 6.3 Jumping when pressing the spacebar
... 公共浮点跳跃力 = 12.0f; ... body.velocity = 运动; ❶ if (Input.GetKeyDown(KeyCode.Space)) { ❷ 身体.添加力(Vector2.up * jumpForce,ForceMode2D.Impulse); } ...
... public float jumpForce = 12.0f; ... body.velocity = movement; ❶ if (Input.GetKeyDown(KeyCode.Space)) { ❷ body.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse); } ...
❶ Existing code to help show where to position new code
❷ Add force only when the spacebar is pressed.
重要的一行是AddForce()命令。代码为 Rigidbody 添加了向上的力,并以脉冲模式执行。脉冲是突然的震动,而不是持续施加的力。当按下空格键时,此代码会施加突然向上的震动。
The important line is the AddForce() command. The code adds an upward force to the Rigidbody and does so in impulse mode. An impulse is a sudden jolt, as opposed to a continuously applied force. This code, then, applies a sudden upward jolt when the spacebar is pressed.
与此同时,重力继续影响跳跃的玩家,导致玩家跳跃时产生漂亮的弧线。不过,你可能已经注意到了另一个问题,所以让我们来解决那。
Meanwhile, gravity continues to affect the jumping player, resulting in a nice arc when the player jumps. You may have noticed another issue, however, so let’s address that.
这跳跃控制有一个微妙的问题:玩家可以在半空中跳跃!如果玩家已经在半空中(无论是因为他们跳跃还是因为他们正在下落),按下空格键都会产生向上的力,但事实并非如此。相反,跳跃控制应该只在玩家在地面时才有效。因此,您需要检测玩家何时在地面上。
The jump control has one subtle problem: the player can jump in midair! If the player is already in midair (either because they jumped or because they are falling), pressing the spacebar applies an upward force, but it shouldn’t. Instead, the jump control should work only when the player is on the ground. You therefore need to detect when the player is on the ground.
Listing 6.4 Checking if the player is on the ground
... 私人BoxCollider2D盒子; ... box = GetComponent<BoxCollider2D>(); ❶ ... 身体.速度 = 运动; Vector3 max = box.bounds.max; Vector3 min = box.bounds.min; Vector2 corner1 = new Vector2(max.x, min.y - .1f); ❷ Vector2 corner2 = new Vector2(min.x, min.y - .2f); ❷ Collider2D 命中 = Physics2D.OverlapArea(角点1,角点2); bool grounded = false; if (hit != null) { ❸ 接地=真实; } if (grounded && Input.GetKeyDown(KeyCode.Space)) { ❹ ...
... private BoxCollider2D box; ... box = GetComponent<BoxCollider2D>(); ❶ ... body.velocity = movement; Vector3 max = box.bounds.max; Vector3 min = box.bounds.min; Vector2 corner1 = new Vector2(max.x, min.y - .1f); ❷ Vector2 corner2 = new Vector2(min.x, min.y - .2f); ❷ Collider2D hit = Physics2D.OverlapArea(corner1, corner2); bool grounded = false; if (hit != null) { ❸ grounded = true; } if (grounded && Input.GetKeyDown(KeyCode.Space)) { ❹ ...
❶ Get this component to use the player’s collider as an area to check.
❷ Check below the collider’s min Y values.
❸ If a collider was detected under the player . . .
❹ . . . add grounded to the jump condition.
有了这段代码,玩家就不能再在半空中跳跃了。脚本中的这一附加部分会检查玩家下方的碰撞器,并在跳跃的条件语句中考虑它们。具体来说,代码首先获取玩家碰撞框的边界,然后在玩家正下方相同宽度的区域中寻找重叠的碰撞器。该检查的结果存储在grounded变量中并用于这有條件的。
With this code in place, the player can no longer jump in midair. This addition to the script checks for colliders below the player and takes them into account in the conditional statement for jumping. Specifically, the code first gets the bounds of the player’s collision box and then looks for overlapping colliders in an area of the same width just below the player. The result of that check is stored in the grounded variable and used in the conditional.
在此时,玩家移动(行走和跳跃)的最重要方面已实现。让我们通过向玩家周围的环境添加新功能来完善此平台游戏演示。
At this point, the most crucial aspects of the player’s movement, walking and jumping, are implemented. Let’s round out this platformer demo by adding new functionality to the environment around the player.
正确的现在,这个演示有正常的水平地板可以站立。不过,平台游戏中使用了许多有趣的平台,所以让我们实现一些其他选项。您将创建的第一个不寻常的地板是斜坡。复制 Floor 对象,将副本的旋转设置为0、0、-25,将其移到左侧(大约-3.47、-1.27、0),并将其命名为Slope。请一直参考图 6.1 来查看它是什么样子。
Right now, this demo has normal, level floors to stand on. Many interesting kinds of platforms are used in platform games, though, so let’s implement a few other options. The first unusual floor you’ll create is a slope. Duplicate the Floor object, set the duplicate’s rotation to 0, 0, -25, move it off to the left side (around -3.47, -1.27, 0), and name it Slope. Refer all the way back to figure 6.1 to see what this looks like.
如果您现在开始游戏,玩家在移动时已经可以正确地上下滑动,但在空闲时会因为重力而缓慢向下滑动。为了解决这个问题,让我们在玩家站在地面上且空闲时关闭重力。幸运的是,您已经检测到地面,因此可以在新代码中重复使用。事实上,只需要一行新代码。
If you play now, the player already slides up and down correctly when moving but slowly slides down because of gravity when idle. To address this, let’s turn off gravity for the player when the player is both standing on the ground and idle. Fortunately, you already detect the ground, so that can be reused in the new code. Indeed, only a single new line is needed.
Listing 6.5 Turning off gravity when standing on the ground
... body.gravityScale = (grounded && Mathf.Approximately(deltaX, 0)) ? 0 : 1; ❶ if (grounded && Input.GetKeyDown(KeyCode.Space)) { ❷ ...
... body.gravityScale = (grounded && Mathf.Approximately(deltaX, 0)) ? 0 : 1; ❶ if (grounded && Input.GetKeyDown(KeyCode.Space)) { ❷ ...
❶ Check both on ground and not moving.
❷ Existing code to help show where to position new code
通过对移动代码进行调整,玩家角色可以正确地在斜坡上行走。接下来,单向平台是平台游戏中常见的另一种不寻常的地板。我说的是你可以跳过但仍能站立的平台;玩家的头会撞到正常的、完全坚固的平台底部。
With that adjustment to the movement code, your player character correctly navigates slopes. Next, one-way platforms are another sort of unusual floor common in platformers. I’m talking about platforms that you can jump through but still stand on; the player bonks their head against the bottom of normal, fully solid platforms.
由于单向平台在平台游戏中相当常见,因此 Unity 提供了单向平台功能。您可能还记得,之前添加 Platform Effector 组件时,单向设置已关闭。现在将其打开!要创建新平台,请复制 Floor 对象,缩放副本10、1、1 ,将其放置在地板上方-1.68、0.11、0左右的位置,然后将对象命名为Platform 。哦,别忘了在Platform Effector 组件中启用 Use One Way 。
Because they’re fairly common in platform games, Unity provides functionality for one-way platforms. As you may recall, when you added the Platform Effector component earlier, a one-way setting was turned off. Now turn that on! To create a new platform, duplicate the Floor object, scale the duplicate 10, 1, 1, place it above the floor around position -1.68, 0.11, 0, and name the object Platform. Oh, and don’t forget to turn on Use One Way in the Platform Effector component.
玩家从下方跳过平台,但从上方下来时站在平台上。我们有一个可能的问题需要修复,如图 6.11 所示。Unity 可能会将平台精灵显示在玩家上方(为了更容易看到这一点,请将 Jump Force 设置为7进行测试),但您可能希望玩家位于上方。您可以像在第 5 章中那样调整玩家的 Z 位置,但这次您将调整其他内容以显示另一个选项。精灵渲染器具有排序顺序,可用于控制哪些精灵出现在顶部。在玩家的 Sprite Renderer 组件中将“图层中的顺序”设置为1。
The player jumps through the platform from below, but stands on it when coming down from above. We have one possible issue to fix, shown in figure 6.11. Unity may display the platform sprite on top of the player (to see this more easily, test with Jump Force set to 7), but you probably want the player on top. You could adjust the player’s Z position as you did in chapter 5, but this time you’ll adjust something else to show another option. Sprite renderers have a sorting order that can be used to control which sprites appear on top. Set Order in Layer to 1 in the Player’s Sprite Renderer component.
Figure 6.11 Platform sprite overlapping the player sprite
这既能解决倾斜地板的问题,也能解决单向平台的问题。我将介绍另一种不寻常的地板,但它要复杂得多。实施。
That takes care of both sloped floors and one-way platforms. I’m going to cover one more sort of unusual floor, but it is significantly more complex to implement.
一个平台游戏中常见的第三种不寻常的地板是移动平台。实现这种平台既需要一个新的脚本来控制平台本身,也需要更改玩家的移动脚本来处理移动平台。您将编写一个脚本,该脚本采用两个位置,即开始和结束,并使平台在它们之间弹跳。首先,创建一个名为MovingPlatform的新 C# 脚本并在其中写入此代码。
A third sort of unusual floor common in platform games is the moving platform. Implementing one requires both a new script to control the platform itself and changes in the player’s movement script to handle moving platforms. You’re going to write a script that takes two positions, start and finish, and makes the platform bounce between them. First, create a new C# script called MovingPlatform and write this code in it.
Listing 6.6 MovingPlatform script for floors that move back and forth
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 MovingPlatform : MonoBehaviour {
公共 Vector3 finishPos = Vector3.zero; ❶
公共浮动速度=0.5f;
私有 Vector3 startPos;
私有浮点数 trackPercent = 0; ❷
私有整数 direction = 1; ❸
无效开始(){
startPos = 变换.位置; ❹
}
无效更新(){
trackPercent += 方向 * 速度 * 时间.deltaTime;
浮点 x = (finishPos.x - startPos.x) * trackPercent + startPos.x;
float y = (finishPos.y - startPos.y) * trackPercent + startPos.y;
变换.位置 = 新 Vector3(x, y,startPos.z);
如果((方向==1&&trackPercent>.9f)||
(方向 == -1 && trackPercent < .1f)){ ❺
方向*=-1;
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MovingPlatform : MonoBehaviour {
public Vector3 finishPos = Vector3.zero; ❶
public float speed = 0.5f;
private Vector3 startPos;
private float trackPercent = 0; ❷
private int direction = 1; ❸
void Start() {
startPos = transform.position; ❹
}
void Update() {
trackPercent += direction * speed * Time.deltaTime;
float x = (finishPos.x - startPos.x) * trackPercent + startPos.x;
float y = (finishPos.y - startPos.y) * trackPercent + startPos.y;
transform.position = new Vector3(x, y, startPos.z);
if ((direction == 1 && trackPercent > .9f) ||
(direction == -1 && trackPercent < .1f)) { ❺
direction *= -1;
}
}
}
❷ How far along the “track” between start and finish
❹ Placement in the scene is the position to move from
❺ Change direction at both start and end.
将此脚本拖到平台对象上。太棒了——播放场景时平台会左右移动!现在您需要调整玩家的移动脚本,以将玩家附加到移动平台上。以下是需要进行的更改。
Drag this script onto the platform object. Great—the platform moves left and right when you play the scene! Now you need to adjust the player’s movement script to attach the player to the moving platform. Here are the changes to make.
清单 6.7 在PlatformerPlayer中处理移动平台
Listing 6.7 Handling moving platforms in PlatformerPlayer
...
身体.添加力(Vector2.up * jumpForce,ForceMode2D.Impulse);
}
移动平台平台=空;
如果 (命中 != 空) {
平台 = hit.GetComponent<MovingPlatform>(); ❶
}
如果 (平台 != null) { ❷
变换.父级 = 平台.变换;
} 别的 {
变换.父级 = 空;
}
anim.SetFloat("速度", Mathf.Abs(deltaX)); ❸
......
body.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
}
MovingPlatform platform = null;
if (hit != null) {
platform = hit.GetComponent<MovingPlatform>(); ❶
}
if (platform != null) { ❷
transform.parent = platform.transform;
} else {
transform.parent = null;
}
anim.SetFloat("speed", Mathf.Abs(deltaX)); ❸
...
❶ Check whether the platform under the player is a moving platform.
❷要么附加到平台,要么清除transform.parent。
❷ Either attach to the platform or clear transform.parent.
❸ Existing code to help show where to position new code
现在,玩家在跳上平台后会随平台一起移动。这一变化主要归结为将玩家作为平台的子对象附加;请记住,当您设置父对象时,子对象会随父对象一起移动。清单 6.7 使用GetComponent()检查检测到的地面是否是移动平台。如果是,它将该平台设置为玩家的父级;否则,玩家将与任何父级分离。
Now the player moves with the platform after jumping on it. This change mostly comes down to attaching the player as a child of the platform; remember, when you set a parent object, the child object moves with the parent. Listing 6.7 uses GetComponent() to check whether the ground detected is a moving platform. If so, it sets that platform as the player’s parent; otherwise, the player is detached from any parent.
但有一个大问题:玩家继承了平台的比例,导致缩放比例奇怪。可以通过反向缩放(缩小玩家比例以抵消平台的放大比例)解决这个问题。
There’s a big problem, though: the player inherits the platform’s scale, resulting in weird scaling. That can be fixed by counter-scaling (scaling the player down to counteract the platform’s scale up).
Listing 6.8 Correcting scaling of the player
...
anim.SetFloat("速度", Mathf.Abs(deltaX));
Vector3 pScale = Vector3.one; ❶
如果(平台!= null){
pScale = 平台.变换.本地尺度;
}
如果 (!Mathf.Approximately(deltaX, 0)) {
变换.localScale = new Vector3(
Mathf.Sign(deltaX) / pScale.x, 1/pScale.y, 1); ❷
}
}
......
anim.SetFloat("speed", Mathf.Abs(deltaX));
Vector3 pScale = Vector3.one; ❶
if (platform != null) {
pScale = platform.transform.localScale;
}
if (!Mathf.Approximately(deltaX, 0)) {
transform.localScale = new Vector3(
Mathf.Sign(deltaX) / pScale.x, 1/pScale.y, 1); ❷
}
}
...
❶ Default scale 1 if not on moving platform
❷ Replace existing scaling with new code.
反缩放的数学运算很简单:将玩家设置为 1 除以平台的比例。当玩家的比例乘以平台的比例时,比例为 1。此代码唯一棘手的部分是乘以移动值的符号;您可能还记得,玩家是根据移动方向翻转的。
The math for counter-scaling is straightforward: set the player to 1 divided by the platform’s scale. When the player’s scale is then multiplied by the platform’s scale, that leaves a scale of 1. The only tricky bit of this code is multiplying by the sign of the movement value; as you may recall from earlier, the player is flipped based on the movement direction.
这就是完全实现的移动平台。这个平台游戏演示只需要一个最终触碰。
And that’s moving platforms fully implemented. This platformer demo needs only one final touch.
移动相机是您要添加到此 2D 平台游戏的最后一个功能。创建一个名为FollowCam的脚本,将其拖到相机上,然后在其中写入以下内容。
Moving the camera is the final feature you’ll add to this 2D platformer. Create a script called FollowCam, drag it onto the camera, and then write the following in it.
Listing 6.9 FollowCam script to move with the player
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 FollowCam : MonoBehaviour {
公共变换目标;
无效 LateUpdate() {
变换.位置 = new Vector3(
目标位置.x, 目标位置.y, 变换位置.z); ❶
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FollowCam : MonoBehaviour {
public Transform target;
void LateUpdate() {
transform.position = new Vector3(
target.position.x, target.position.y, transform.position.z); ❶
}
}
❶ Preserve the Z position while changing X and Y.
编写代码后,将 Player 对象拖到 Inspector 中脚本的目标槽中。播放场景,摄像机四处移动,让玩家保持在屏幕中央。您可以看到代码将目标对象的位置应用于摄像机,并将玩家设置为目标对象。请注意,方法名称为LateUpdate()而不是Update();这是 Unity 识别的另一个名称。LateUpdate ()也每帧执行,但它在每帧Update()之后执行。
With that code written, drag the Player object to the script’s target slot in the Inspector. Play the scene, and the camera moves around, keeping the player at the center of the screen. You can see that the code applies the target object’s position to the camera, and you set the player as the target object. Note that the method name is LateUpdate() instead of Update(); that’s yet another name Unity recognizes. LateUpdate() also executes every frame, but it happens after Update() every frame.
相机始终与玩家精确移动,这有点不协调。大多数平台游戏中的相机都有各种微妙但复杂的行为,随着玩家移动,突出显示关卡的不同部分。事实上,平台游戏的相机控制是一个令人惊讶的深奥话题;尝试搜索“平台游戏相机”并查看所有结果。不过,在这种情况下,你只需要让相机的移动更平滑、更不协调;此列表进行了调整。
It’s slightly jarring that the camera moves exactly with the player at all times. The camera in most platformers has all kinds of subtle but complicated behavior, highlighting different parts of the level as the player moves around. In fact, camera control for platform games is a surprisingly deep topic; try searching for “platform game camera” and see all the results. In this case, though, you’re just going to make the camera’s movement smoother and less jarring; this listing makes that adjustment.
Listing 6.10 Smoothing the camera movement
...
公共浮点平滑时间 = 0.2f;
私有 Vector3 速度 = Vector3.zero;
...
无效 LateUpdate() {
Vector3 目标位置 = new Vector3(
目标位置.x, 目标位置.y, 变换位置.z); ❶
变换.位置 = Vector3.SmoothDamp(变换.位置,
目标位置,参考速度,平滑时间); ❷
}
......
public float smoothTime = 0.2f;
private Vector3 velocity = Vector3.zero;
...
void LateUpdate() {
Vector3 targetPosition = new Vector3(
target.position.x, target.position.y, transform.position.z); ❶
transform.position = Vector3.SmoothDamp(transform.position,
targetPosition, ref velocity, smoothTime); ❷
}
...
❶ Preserve Z position while changing X and Y.
❷ Smooth transition from current to target position
主要的变化是调用一个名为SmoothDamp的函数();其他更改(如添加时间和速度变量)都是为了支持该功能。这是 Unity 提供的功能,用于使值平滑过渡到新值。在这种情况下,值是相机和目标的位置。
The main change is calling a function called SmoothDamp(); the other changes (like adding time and velocity variables) are all to support that function. That’s a function Unity provides for making values smoothly transition to a new value. In this case, the values are the positions of the camera and target.
现在摄像机可以顺畅地跟随玩家移动。您实现了玩家的移动、几种平台,现在还有摄像机控制。看起来像本章的项目是完成的!
The camera moves smoothly with the player now. You implemented the player’s movement, several kinds of platforms, and now camera control. Looks like this chapter’s project is finished!
在本章中,您将为 3D 游戏构建 2D 界面显示。到目前为止,我们在构建第一人称演示时专注于虚拟场景本身。但除了游戏发生的虚拟场景之外,每个游戏都需要抽象的交互和信息显示。这适用于所有游戏,无论是 2D 还是 3D,第一人称射击游戏还是益智游戏。因此,虽然本章中的技术将用于 3D 游戏,但它们也适用于 2D 游戏。
In this chapter, you’ll build a 2D interface display for a 3D game. So far, we’ve focused on the virtual scene itself while building a first-person demo. But every game needs abstract interaction and information displays in addition to the virtual scene the gameplay takes place in. This is true for all games, whether they’re 2D or 3D, first-person shooters or puzzle games. So, while the techniques in this chapter will be used on a 3D game, they apply to 2D games as well.
这些抽象的交互显示被称为UI,或者更具体地说,GUI。 GUI(图形用户界面的缩写)是指界面的可视部分,例如文本和按钮(见图 7.1)。从技术上讲,UI 包括非图形控件,例如键盘或游戏手柄,但人们在说“用户界面”时往往指的是图形部分。
These abstract interaction displays are referred to as the UI, or more specifically, the GUI. GUI (short for Graphical User Interface) refers to the visual part of the interface, such as text and buttons (see figure 7.1). Technically, the UI includes nongraphical controls, such as the keyboard or game pad, but people tend to be referring to the graphical parts when they say “user interface.”
Figure 7.1 The GUI you’ll create for a game
虽然任何软件都需要某种 UI 以便用户控制它,但游戏使用 GUI 的方式通常与其他软件略有不同。例如,在网站中,GUI 基本上就是网站(就视觉表现而言)。但在游戏中,文本和按钮通常是游戏视图顶部的附加覆盖层,这种显示称为平视显示器( HUD )。
Although any software requires some sort of UI in order for the user of that software to control it, games often use their GUI in a slightly different way from other software. In a website, for example, the GUI basically is the website (in terms of visual representation). In a game, though, text and buttons are often an additional overlay on top of the Game view, a kind of display called a heads-up display (HUD).
定义平视显示器( HUD ) 将图形叠加在世界视图之上。HUD 的概念起源于军用喷气式飞机 — 其目的是使飞行员无需低头就能看到关键信息。同样,叠加在游戏视图上的 GUI 称为 HUD。
DEFINITION A heads-up display (HUD) superimposes graphics on top of the view of the world. The concept of a HUD originated with military jets—its purpose was to enable pilots to see crucial information without having to look down. Similarly, a GUI superimposed on the Game view is referred to as the HUD.
本章介绍如何使用 Unity 中的 UI 工具构建游戏的 HUD。正如您在第 5 章中看到的,Unity 提供了多种创建 UI 显示的方法。本章演示了取代 Unity 第一个 UI 系统的高级 UI 系统。我还讨论了以前的 UI 系统和新系统的优势。
This chapter shows how to build the game’s HUD by using the UI tools in Unity. As you saw in chapter 5, Unity provides multiple ways to create UI displays. This chapter demonstrates the advanced UI system that replaced Unity’s first UI system. I also discuss the previous UI system and the advantages of the newer system.
要了解 Unity 中的 UI 工具,您需要在第 3 章的 FPS 项目基础上进行构建。本章中的项目涉及以下步骤:
To learn about the UI tools in Unity, you’ll build on top of the FPS project from chapter 3. The project in this chapter involves these steps:
从第 3 章复制项目并打开副本以开始本章的工作。与往常一样,您需要的艺术资产位于示例下载中。设置好这些文件后,您就可以开始构建游戏的 UI 了。
Copy the project from chapter 3 and open the copy to start working in this chapter. As usual, the art assets you need are in the sample download. With those files set up, you’re ready to start building the game’s UI.
注意本章中的所有示例都是在第 3 章中创建的 FPS 游戏的基础上构建的。但本章的内容在很大程度上独立于该基础项目;我们只会在现有游戏演示的基础上添加一个图形界面。虽然我建议您下载第 3 章的项目,但您可以自由使用任何您喜欢的游戏演示。
NOTE All the examples in this chapter are built on top of the FPS game created in chapter 3. But the content of this chapter is largely independent of that base project; we’ll just add a graphical interface on top of the existing game demo. Although I’ve suggested that you download the chapter 3 project, you’re free to use whatever game demo you’d like.
要开始构建 HUD,您首先需要了解 UI 系统的工作原理。Unity 提供了多种构建游戏 HUD 的方法,因此我们需要了解这些系统的工作原理。然后我们可以简要规划 UI 并准备所需的艺术资产。
To start building the HUD, you first need to understand how the UI system works. Unity provides multiple approaches to building a game’s HUD, so we need to go over how those systems work. Then we can briefly plan the UI and prepare the art assets that we’ll need.
从在其第一个版本中,Unity 配备了即时模式 GUI 系统。即时模式系统可轻松在屏幕上放置可点击按钮。清单 7.1 显示了执行此操作的代码:只需将此脚本附加到场景中的任何对象即可。
From its first version, Unity has come with an immediate mode GUI system. The immediate mode system makes it easy to put a clickable button on the screen. Listing 7.1 shows the code to do that: simply attach this script to any object in the scene.
定义 立即模式是指每帧都明确发出绘制命令,而不是一次定义所有视觉效果,然后系统知道每帧要绘制什么,而无需您再次告诉它。后一种方法称为保留模式。
DEFINITION Immediate mode refers to explicitly issuing draw commands every frame—instead of defining all the visuals once, and then for every frame the system knows what to draw without you having to tell it again. The latter approach is called retained mode.
有关即时模式 UI 的另一个示例,请回忆一下第 3 章中显示的目标光标。这个 GUI 系统完全基于代码,无需在 Unity 的编辑器中完成任何工作。
For another example of immediate mode UI, recall the target cursor displayed in chapter 3. This GUI system is entirely based on code, with no work done in Unity’s editor.
Listing 7.1 Example of a button using the immediate mode GUI
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类BasicUI:MonoBehaviour {
void OnGUI() { ❶
if (GUI.Button(new Rect(10, 10, 40, 20), "Test")) { ❷
Debug.Log("测试按钮");
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BasicUI : MonoBehaviour {
void OnGUI() { ❶
if (GUI.Button(new Rect(10, 10, 40, 20), "Test")) { ❷
Debug.Log("Test button");
}
}
}
❶ Function called every frame after everything else renders
❷ Parameters: position X, pos Y, width, height, text label
此清单中的代码核心是OnGUI()方法. 与Start()和Update()非常相似,每个MonoBehaviour都会自动响应OnGUI()。该函数在渲染 3D 场景后每帧运行一次,提供放置 GUI 绘制命令的位置。此代码绘制一个按钮;请注意,按钮的命令每帧执行一次(即,在立即模式样式中)。按钮命令用于条件,当单击按钮时会做出响应。
The core of the code in this listing is the OnGUI() method. Much like Start() and Update(), every MonoBehaviour automatically responds to OnGUI(). That function runs every frame after the 3D scene is rendered, providing a place to put GUI drawing commands. This code draws a button; note that the command for a button is executed every frame (that is, in immediate mode style). The button command is used in a conditional that responds when the button is clicked.
由于即时模式 GUI 可以轻松以最小的努力在屏幕上显示几个按钮,因此我们有时会在以后的章节中使用它作为示例。但是默认按钮几乎是唯一使用该系统容易创建的东西,因此 Unity 的最新版本现在有一个基于编辑器中布局的 2D 图形的新界面系统。这个较新的界面系统需要更多的努力来设置,但您可能希望在完成的游戏中使用它,因为它可以产生更精致的结果。
Because the immediate mode GUI makes it easy to get a few buttons onscreen with minimal effort, we’ll sometimes use it for examples in future chapters. But default buttons are about the only thing easy to create with that system, so more recent versions of Unity now have a new interface system based on 2D graphics laid out in the editor. This newer interface system takes a bit more effort to set up, but you’ll probably want to use it in finished games because it produces more polished results.
较新的 UI 系统在保留模式下工作,因此图形只需布局一次,然后每帧绘制一次,而无需不断重新定义。在此系统中,UI 的图形放置在 Unity 的编辑器中。与即时模式 UI 相比,这有两个优点:(1) 您可以在放置 UI 元素时看到 UI 的外观,(2) 此系统让您可以直接使用自己的图像自定义 UI。
The newer UI system works in retained mode, so the graphics are laid out once and then drawn every frame without needing to be continually redefined. In this system, graphics for the UI are placed in Unity’s editor. This provides two advantages over the immediate mode UI: (1) you can see what the UI looks like while placing UI elements, and (2) this system makes it straightforward to customize the UI with your own images.
注意:第 1 章提到 Unity 有三种 UI 系统(在http://mng.bz/205X上进行了比较),因为后续开发的系统在其前身的基础上进行了改进。本书将介绍第二种 UI 系统(Unity UI 或 uGUI),因为它仍然比不完整的第三种 UI 系统(UI Toolkit)更受欢迎。
NOTE Chapter 1 mentioned that Unity has three UI systems (which are compared at http://mng.bz/205X) because successively developed systems improved on their predecessor. This book covers the second UI system (Unity UI, or uGUI) because it is still preferred over the incomplete third UI system (UI Toolkit).
要使用这个系统,你需要导入图像,然后将对象拖到场景中。接下来,让我们规划一下这个 UI 将如何看。
To use this system, you’re going to import images and then drag objects into the scene. Next, let’s plan how this UI will look.
这大多数游戏的 HUD 都包含一些重复的 UI 控件。因此,这个项目不需要太复杂,您就能学会如何构建游戏的 UI。您将在主游戏视图的屏幕角落放置一个分数显示和一个“设置”按钮(见图 7.2)。“设置”按钮将弹出一个弹出窗口,其中包含一个文本字段和一个滑块。
The HUD for most games comprises a few UI controls repeated over and over. Therefore, this project doesn’t need to be terribly complex in order for you to learn how to build a game’s UI. You’re going to put a score display and a Settings button in the corners of the screen over the main Game view (see figure 7.2). The Settings button will bring up a pop-up window, which will have both a text field and a slider.
对于此示例,这些输入控件将用于设置玩家的姓名和移动速度,但最终这些 UI 元素可以控制与游戏相关的任何设置。好吧,这个计划非常简单!下一步是引入需要。
For this example, those input controls will be used for setting the player’s name and movement speed, but ultimately those UI elements could control any settings relevant to your game. Well, that plan is pretty simple! The next step is bringing in the images that are needed.
这UI 需要一些图像来显示按钮等内容。UI 是由 2D 图像构建的,就像第 5 章中的图形一样,因此您将遵循相同的两个步骤:
This UI requires some images to display for things like buttons. The UI is built from 2D images like the graphics in chapter 5, so you’ll follow the same two steps:
要完成这些步骤,首先将图像拖到“项目”视图中以导入它们。然后在“检查器”中,将其“纹理类型”设置更改为“Sprite (2D 和 UI)”。
To accomplish these steps, first drag the images into Project view to import them. Then, in the Inspector, change their Texture Type setting to Sprite (2D and UI).
警告:纹理类型设置默认为 3D 项目中的纹理和 2D 项目中的精灵。如果您想要 3D 项目中的精灵,则需要手动调整此设置。
WARNING The Texture Type setting defaults to Texture in 3D projects and to Sprite in 2D projects. If you want sprites in a 3D project, you need to manually adjust this setting.
从示例下载中获取所有必要的图像(见图 7.3),然后将它们导入到您的项目中。确保所有导入的资产都设置为 Sprite;您可能需要在导入后显示的设置中调整纹理类型。
Get all the necessary images from the sample download (see figure 7.3) and then import them into your project. Make sure all the imported assets are set to Sprite; you’ll probably need to adjust Texture Type in the settings displayed after importing.
Figure 7.3 Images that are needed for this chapter’s project
这些精灵包括您将要创建的按钮、分数显示和弹出窗口。现在图像已导入,让我们将这些图形放到屏幕。
These sprites comprise the buttons, score display, and pop-up that you’ll create. Now that the images are imported, let’s put these graphics onto the screen.
这艺术资产与我们在第 5 章中使用的 2D 精灵相同,但我们在场景中使用这些资产的方式略有不同。Unity 提供了特殊工具来将图像制作成显示在 3D 场景上的 HUD,而不是将图像显示为场景的一部分。由于显示需求可能会在不同屏幕上发生变化,因此在定位 UI 元素时会使用一些特殊技巧。
The art assets are the same kind of 2D sprites we used in chapter 5, but we’ll use those assets in the scene a bit differently. Unity provides special tools to make the images a HUD that’s displayed over the 3D scene, rather than displaying the images as part of the scene. Some special tricks are used when positioning UI elements, because of the needs of a display that may change on different screens.
一UI 系统工作方式的最基本和最不明显的方面之一是,所有图像都必须附加到画布对象。
One of the most fundamental and nonobvious aspects of how the UI system works is that all images must be attached to a canvas object.
提示Canvas 是 Unity 渲染为游戏 UI 的一种特殊对象。
TIP Canvas is a special kind of object that Unity renders as the UI for a game.
打开 GameObject 菜单查看可以创建的对象;在 UI 类别中选择 Canvas。场景中将出现一个画布对象(将该对象重命名为HUD Canvas可能会更清楚)。此对象代表整个屏幕的范围,与 3D 场景相比,它非常大,因为它将屏幕的一个像素缩放为场景中的一个单位。
Open the GameObject menu to see the objects you can create; in the UI category, choose Canvas. A canvas object will appear in the scene (it may be clearer to rename the object HUD Canvas). This object represents the entire extent of the screen, and it’s huge compared to the 3D scene because it scales one pixel of the screen to one unit in the scene.
警告:创建画布对象时,也会自动创建一个 EventSystem 对象。该对象是 UI 交互所必需的,但您可以忽略它。
WARNING When you create a canvas object, an EventSystem object is automatically created too. That object is required for UI interaction, but you can otherwise ignore it.
切换到 2D 视图模式(参见图 7.4),然后双击层次结构中的画布以缩小并完整查看。当整个项目都是 2D 时,2D 视图模式是自动的,但在 3D 项目中,必须单击此切换按钮才能在 UI 和主场景之间切换。要返回查看 3D 场景,请关闭 2D 视图模式,然后双击建筑物以放大该对象。
Switch to 2D view mode (refer to figure 7.4) and double-click the canvas in the Hierarchy to zoom out and view it fully. The 2D view mode is automatic when the entire project is 2D, but in a 3D project, this toggle must be clicked to switch between the UI and the main scene. To return to viewing the 3D scene, toggle off the 2D view mode and then double-click the building to zoom in to that object.
Figure 7.4 A blank canvas object in the Scene view
提示不要忘记第 4 章中的这个提示:场景视图窗格顶部有控制可见内容的按钮,因此请在那里找到“效果”按钮来关闭天空盒。
TIP Don’t forget this tip from chapter 4: across the top of the Scene view’s pane are buttons that control what’s visible, so look there for the Effects button to turn off the skybox.
画布具有可以调整的设置。第一个是渲染模式选项。保留默认设置(屏幕空间—覆盖),但您应该知道三种可能的设置的含义:
The canvas has settings that you can adjust. The first is the Render Mode option. Leave this at the default setting (Screen Space—Overlay), but you should know what the three possible settings mean:
Screen Space —Overlay —Renders the UI as 2D graphics on top of the camera view. (This is the default setting.)
Screen Space —Camera —Also renders the UI on top of the camera view, but UI elements can rotate for perspective effects.
World Space —Places the canvas object within the scene, as if the UI were part of the 3D scene.
除初始默认模式之外的两种模式有时可以用于特定效果,但稍微复杂一些。
The two modes besides the initial default can sometimes be useful for specific effects but are slightly more complicated.
另一个重要设置是 Pixel Perfect。此设置使渲染巧妙地调整图像的位置,使它们始终保持完美清晰锐利(而不是在像素之间放置时模糊)。继续并选中该复选框。现在 HUD 画布已设置好,但它仍然是空白的,需要精灵。
The other important setting is Pixel Perfect. This setting causes the rendering to subtly adjust the position of images so that they’re always perfectly crisp and sharp (as opposed to blurring them when positioned between pixels). Go ahead and select that check box. Now the HUD canvas is set up, but it’s still blank and needs sprites.
这canvas 对象定义了一个显示为 UI 的区域,但它仍然需要精灵来显示。参考图 7.2 中的 UI 模型,您将在左上角看到方块/敌人的图像,旁边是显示分数的文本,右上角是一个齿轮状的按钮。因此,GameObject 菜单的 UI 部分包含创建图像、文本或按钮的选项。创建每个选项中的一个,但在适用时使用 TextMeshPro 版本。也就是说,选择 GameObject > UI > Image,然后选择 Text - TextMeshPro,然后选择 Button - TextMeshPro。
The canvas object defines an area to display as the UI, but it still requires sprites to display. Referring to the UI mock-up in figure 7.2, you’ll see an image of the block/enemy in the top-left corner, text displaying the score next to that, and a gear-shaped button in the top-right corner. Accordingly, the UI section of the GameObject menu contains options to create an image, text, or button. Create one of each, but using the TextMeshPro version when applicable. That is, choose GameObject > UI > Image, then Text - TextMeshPro, then Button - TextMeshPro.
注意:与第 5 章一样,您需要安装 TextMeshPro 包,因此如果 UI 对象菜单中未显示 TextMeshPro 版本,请转到窗口 > 包管理器。首次创建 TextMeshPro 对象时,TMP 导入器窗口将自动出现。单击导入 TMP 基本功能按钮。
NOTE Just as in chapter 5, you need to have the TextMeshPro package installed, so go to Window > Package Manager if no TextMeshPro versions are displayed in the menu of UI objects. The TMP Importer window will automatically appear when you create a TextMeshPro object for the first time. Click the Import TMP Essentials button.
为了正确显示,UI 元素需要是画布对象的子项。Unity 会自动执行此操作,但请记住,像往常一样,您可以在层次结构视图周围拖动对象以建立父子链接(见图 7.5)。
To display correctly, UI elements need to be a child of the canvas object. Unity does this automatically, but remember that, as usual, you can drag objects around the Hierarchy view to make parent-child linkages (see figure 7.5).
Figure 7.5 Canvas with an image linked in the Hierarchy view
画布内的对象可以作为父对象一起定位,就像场景中的任何其他对象一样。例如,您应该将文本对象拖到图像上,这样文本就会随图像移动。默认按钮对象也有一个文本对象作为其子对象,但此项目的按钮不需要文本标签,因此请删除默认文本对象。
Objects within the canvas can be parented together for positioning purposes, just like any other objects in the scene. For example, you should drag the text object onto the image so that the text will move with the image. The default button object also has a text object as its child, but this project’s button doesn’t need a text label, so delete the default text object.
将 UI 元素粗略地定位到其角落。在下一节中,我们将精确定位;现在,只需拖动对象,直到它们基本就位。单击并将图像对象拖到画布的左上角;按钮位于右上角。
Roughly position the UI elements into their corners. In the next section, we’ll make the positions exact; for now, just drag the objects until they’re pretty much in position. Click and drag the image object to the top left of the canvas; the button goes in the top right.
提示如第 5 章所述,您可以在 2D 模式下使用 Rect 工具。我将其描述为一个包含所有三种变换的单一操作工具:移动、旋转和缩放。这些操作在 3D 中必须是单独的工具,但在 2D 中组合在一起,因为这样可以少担心一个维度。在 2D 模式下,此工具会自动选择,或者您可以单击 Unity 左上角附近的按钮。
TIP As noted in chapter 5, you use the Rect tool in 2D mode. I described it as a single manipulation tool that encompasses all three transforms: Move, Rotate, and Scale. These operations have to be separate tools in 3D but are combined in 2D because that’s one less dimension to worry about. In 2D mode, this tool is selected automatically, or you can click the button near the top-left corner of Unity.
目前,图像是空白的。如果您选择一个 UI 对象并查看检查器,您应该会在图像组件顶部附近看到一个源图像插槽。如图 7.6 所示,从项目视图中拖过精灵(记住,不是纹理!)以将图像分配给对象。将敌人精灵分配给图像对象,将齿轮精灵分配给按钮对象(分配精灵后单击设置原生大小以正确调整图像对象的大小)。
At the moment, the images are blank. If you select a UI object and look at the Inspector, you should see a Source Image slot near the top of the image component. As shown in figure 7.6, drag over sprites (remember, not textures!) from the Project view to assign images to the objects. Assign the enemy sprite to the image object, and the gear sprite to the button object (click Set Native Size after assigning sprites to properly size the image object).
图 7.6 将 2D 精灵分配给 UI 元素的 Image 属性。
Figure 7.6 Assigning 2D sprites to the Image property of UI elements.
这样就解决了敌人图像和齿轮按钮的外观问题。至于文本对象,检查器中有一系列设置(见图 7.7)。首先,在大型文本输入框中输入一个数字;此文本稍后将被覆盖,但它很有用,因为它看起来像编辑器中的分数显示。文本大小不对,因此将字体大小更改为24。还要单击第一个字体样式按钮以将其设置为粗体,然后将顶点颜色更改为黑色。您还需要将此标签设置为左水平对齐和中间垂直对齐。目前,其余设置可以保留其默认值。
That took care of the appearance of both the enemy image and the gear button. As for the text object, the Inspector has a bunch of settings (see figure 7.7). First, type a single number in the large Text Input box; this text will be overwritten later, but it’s useful because it looks like a score display within the editor. The text is the wrong size, so change the Font Size to 24. Also click the first Font Style button for Bold, and then change Vertex Color to black. You also want to set this label to left horizontal alignment and middle vertical alignment. For now, the remaining settings can be left at their default values.
Figure 7.7 Settings for a UI text object
注意:我们刚刚没有提到的最常调整的属性是字体。要将 TrueType 字体与 TextMeshPro 一起使用,请先将字体导入 Unity,然后选择 Window > TextMeshPro > Font Asset Creator。
NOTE The most commonly adjusted property that we didn’t just touch on is the font. To use a TrueType font with TextMeshPro, first import the font into Unity and then choose Window > TextMeshPro > Font Asset Creator.
现在,精灵已分配给 UI 图像,并且分数文本已设置,您可以单击“播放”以查看 3D 游戏顶部的 HUD。Unity 编辑器中显示的画布显示了屏幕的边界,并且 UI 元素被绘制到屏幕上,如图 7.8 所示。
Now that sprites have been assigned to the UI images and the score text is set up, you can click Play to see the HUD on top of the 3D game. The canvas displayed in Unity’s editor shows the bounds of the screen, and UI elements are drawn onto the screen in the positions shown in figure 7.8.
图 7.8 编辑器中显示的 GUI(左)和游戏运行时显示的 GUI(右)
Figure 7.8 The GUI as seen in the editor (left) and when playing the game (right)
太棒了,你制作了一个在 3D 游戏上显示 2D 图像的 HUD!还有一个更复杂的视觉设置:相对于帆布。
Great, you made a HUD with 2D images displayed over the 3D game! One more complex visual setting remains: positioning UI elements relative to the canvas.
全部UI 对象有一个锚点,在编辑器中显示为 X 形状(见图 7.9)。锚点是一种在 UI 上定位对象的灵活方法。
All UI objects have an anchor, displayed in the editor as an X shape (see figure 7.9). An anchor is a flexible way of positioning objects on the UI.
Figure 7.9 The anchor point of an image object
定义对象的锚点是对象附着在画布或屏幕上的点。该对象的位置是相对于锚点测量的。
DEFINITION The anchor of an object is the point where an object attaches to the canvas or screen. That object’s position is measured relative to the anchor.
位置是类似“x 轴上的 50 像素”的值。但这留下了一个问题:距离什么 50 像素?这就是锚点的作用所在。锚点的目的是让对象相对于锚点保持原位,而锚点则相对于画布移动。锚点被定义为“屏幕中心”之类的东西,因此当屏幕大小发生变化时,锚点将保持居中。同样,将锚点设置在屏幕的右侧将使对象固定在右侧,即使屏幕大小发生变化(例如,如果在不同的显示器上玩游戏)。
Positions are values like “50 pixels on the x-axis.” But that leaves the question: 50 pixels from what? This is where anchors come in. The purpose of an anchor is to keep the object in place relative to the anchor point, whereas the anchor moves around relative to the canvas. The anchor is defined as something like “center of the screen,” and then the anchor will stay centered while the screen changes size. Similarly, setting the anchor to the right-hand side of the screen will keep the object rooted to the right-hand side even if the screen changes size (for example, if the game is played on different monitors).
要理解我所说的内容,最简单的方法就是观察它的实际效果。选择图像对象并查看检查器。锚点设置将显示在变换组件的正下方(见图 7.10)。默认情况下,UI 元素的锚点设置为中心,但您需要将此图像的锚点设置为左上角;图 7.10 显示了如何使用锚点预设进行调整。
The easiest way to understand what I’m talking about is to see it in action. Select the image object and look over at the Inspector. Anchor settings will appear right below the transform component (see figure 7.10). By default, UI elements have their anchor set to Center, but you want to set the anchor to Top Left for this image; figure 7.10 shows how to adjust that by using the Anchor Presets.
Figure 7.10 How to adjust anchor settings
也更改齿轮按钮的锚点。将此对象设置为右上角;单击右上角的锚点预设。现在尝试左右缩放窗口:单击并拖动游戏视图的侧面。由于锚点的存在,当画布改变大小时,UI 对象将停留在其角落。如图 7.11 所示,这些 UI 元素现在在屏幕移动时固定在原地。
Change the gear button’s anchor as well. Set it to Top Right for this object; click the top-right Anchor Preset. Now try scaling the window left and right: click and drag on the side of the game’s view. Thanks to the anchors, the UI objects will stay in their corners while the canvas changes size. As figure 7.11 shows, these UI elements are now rooted in place while the screen moves.
Figure 7.11 Anchors stay in place while the screen changes size.
提示锚点可以调整比例和位置。我们不会在本章中探讨该功能,但图像的每个角都可以固定在屏幕的不同角上。在图 7.11 中,图像没有改变大小,但我们可以调整锚点,这样当屏幕改变大小时,图像也会随之拉伸。
TIP Anchor points can adjust scale as well as position. We’re not going to explore that functionality in this chapter, but each corner of the image can be rooted to a different corner of the screen. In figure 7.11, the images didn’t change size, but we could adjust the anchors so that when the screen changes size, the image stretches with it.
All of the visual setup is done, so it’s time to program interactivity.
前要与 UI 交互,您需要有鼠标光标。您可能还记得,我们调整了光标设置在Start()方法中RayShooter代码。这些设置会锁定并隐藏鼠标光标,这种行为适用于 FPS 游戏中的控件,但会干扰 UI 的使用。从RayShooter中删除这些行,以便您可以单击 HUD。
Before you can interact with the UI, you need to have a mouse cursor. As you may recall, we adjusted Cursor settings in the Start() method of the RayShooter code. Those settings lock and hide the mouse cursor, a behavior that works for the controls in an FPS game but that interferes with using the UI. Remove those lines from RayShooter so that you can click the HUD.
当您打开RayShooter时,您还可以确保在与 GUI 交互时不射击。以下是代码。
While you have RayShooter open, you could also make sure not to shoot while interacting with the GUI. Here is the code for that.
清单 7.2 在RayShooter的代码中添加 GUI 检查
Listing 7.2 Adding a GUI check to the code in RayShooter
使用 UnityEngine.EventSystems; ❶ ... 无效更新(){ 如果 (Input.GetMouseButtonDown(0) && ❷ !EventSystem.current.IsPointerOverGameObject()) { ❸ Vector3 point = new Vector3( 相机.像素宽度/2,相机.像素高度/2,0); ...
using UnityEngine.EventSystems; ❶ ... void Update() { if (Input.GetMouseButtonDown(0) && ❷ !EventSystem.current.IsPointerOverGameObject()) { ❸ Vector3 point = new Vector3( camera.pixelWidth/2, camera.pixelHeight/2, 0); ...
❶ Include UI system code frameworks.
❷ Italicized code was already in script; shown for reference.
❸ Check that GUI isn’t being used.
现在您可以玩游戏并单击按钮,尽管它还没有执行任何操作。您可以观察鼠标悬停在按钮上并单击时按钮的色调变化。此鼠标悬停和单击行为是默认色调,可以为每个按钮更改,但默认行为目前看起来不错。您可以加快默认淡入淡出行为;淡入淡出持续时间是按钮组件中的设置,因此请尝试将其减小到0.01以查看按钮如何变化。
Now you can play the game and click the button, although it doesn’t do anything yet. You can watch the tinting of the button change as you mouse over it and click. This mouseover and click behavior is a default tint that can be changed for each button, but the default looks fine for now. You could speed up the default fading behavior; Fade Duration is a setting in the button component, so try decreasing that to 0.01 to see how the button changes.
提示有时,UI 的默认交互控件也会干扰游戏。还记得与画布一起自动创建的 EventSystem 对象吗?该对象控制 UI 交互控件,默认情况下,它使用箭头键与 GUI 交互。您可能需要关闭箭头键以避免意外与 GUI 交互:为此,请在 EventSystem 的设置中取消选中“发送导航事件”复选框。
TIP Sometimes the default interaction controls of the UI also interfere with the game. Remember the EventSystem object that was created automatically along with the canvas? That object controls the UI interaction controls, and by default it uses the arrow keys to interact with the GUI. You may need to turn off the arrow keys to avoid interacting with the GUI by accident: to do this, deselect the Send Navigation Event check box in the settings for EventSystem.
但是当您单击按钮时不会发生任何其他事情,因为您尚未将其链接到任何代码。接下来让我们处理这个问题。
But nothing else happens when you click the button because you haven’t yet linked it up to any code. Let’s take care of that next.
在一般来说,UI 交互是通过一系列标准步骤进行编程的,这些步骤对于所有 UI 元素都是相同的:
In general, UI interaction is programmed with a standard series of steps that’s the same for all UI elements:
Create a UI object in the scene (the button created in the previous section).
Link UI elements (such as buttons) to the object with that script.
要执行这些步骤,首先我们需要创建一个控制器对象来链接到按钮。创建一个名为UIController的脚本并将该脚本拖到场景中的控制器对象上。
To follow these steps, first we need to create a controller object to link to the button. Create a script called UIController and drag that script onto the controller object in the scene.
Listing 7.3 UIController script used to program buttons
使用System.Collections; 使用 System.Collections.Generic; 使用 UnityEngine; 使用TMPro; ❶ 公共类 UIController : MonoBehaviour { [SerializeField] TMP_Text 分数标签; ❷ 无效更新(){ 分数标签.文本 = 时间.realtimeSinceStartup.ToString(); } 公共无效OnOpenSettings(){ ❸ Debug.Log("打开设置"); } }
using System.Collections; using System.Collections.Generic; using UnityEngine; using TMPro; ❶ public class UIController : MonoBehaviour { [SerializeField] TMP_Text scoreLabel; ❷ void Update() { scoreLabel.text = Time.realtimeSinceStartup.ToString(); } public void OnOpenSettings() { ❸ Debug.Log("open settings"); } }
❶ Import the TextMeshPro code framework.
❷ Reference the Text object in the scene to set the text property.
❸ Method called by Settings button
提示您可能想知道为什么我们需要为SceneController和UIController提供单独的对象。事实上,这个场景非常简单,您可以用一个控制器来处理 3D 场景和 UI。然而,随着游戏变得越来越复杂,将 3D 场景和 UI 作为单独的模块进行间接通信将变得越来越有用。这个概念远远超出了游戏的范围,延伸到了一般的软件:软件工程师将此原则称为关注点分离。
TIP You might be wondering why we need separate objects for SceneController and UIController. Indeed, this scene is so simple that you could have one controller handling both the 3D scene and the UI. As the game gets more complex, though, it’ll become increasingly useful for the 3D scene and the UI to be separate modules, communicating indirectly. This notion extends well beyond games to software in general: software engineers refer to this principle as separation of concerns.
现在将对象拖到组件槽中以将它们连接起来。将 Score 标签(我们之前创建的文本对象)拖到UIController文本槽中。UIController 中的代码设置该标签上显示的文本。目前,代码显示一个计时器来测试文本显示;稍后将更改为分数。
Now drag objects to component slots to wire them up. Drag the Score label (the text object we created before) to the UIController text slot. The code in UIController sets the text displayed on that label. Currently, the code displays a timer to test the text display; that will later be changed to the score.
接下来,向按钮添加一个OnClick条目,以便将控制器对象拖到该按钮上。选择按钮以在检查器中查看其设置。在底部,您应该会看到一个 On Click 面板;最初该面板是空的,但您可以单击 + 按钮添加条目(如图 7.12 所示)。每个条目都定义了一个单击该按钮时调用的函数;列表中既有对象的插槽,也有要调用该函数的菜单。将控制器对象拖到对象插槽,然后在菜单中查找UIController ;在该部分中选择OnOpenSettings() 。
Next, add an OnClick entry to the button to drag the controller object onto. Select the button to see its settings in the Inspector. Toward the bottom, you should see an On Click panel; initially that panel is empty, but you can click the + button to add an entry (as you can see in figure 7.12). Each entry defines a single function that gets called when that button is clicked; the listing has both a slot for an object and a menu for the function to call. Drag the controller object to the object slot, and then look for UIController in the menu; select OnOpenSettings() in that section.
Figure 7.12 The On Click panel toward the bottom of the button settings
玩游戏并单击按钮以在控制台中输出调试消息。同样,代码当前是随机输出以测试按钮的功能。我们想要打开一个设置弹出窗口,所以让我们创建该弹出窗口下一个。
Play the game and click the button to output debug messages in the console. Again, the code is currently random output to test the button’s functionality. We want to open a settings pop-up, so let’s create that pop-up window next.
这UI 有一个按钮可以打开弹出窗口,但目前还没有弹出窗口。这将是一个新的图像对象,以及附加到该对象的几个控件(例如按钮和滑块)。第一步是创建一个新图像,因此选择 GameObject > UI > Image。与之前一样,新图像在 Inspector 中有一个名为 Source Image 的插槽。将精灵拖到该插槽以设置此图像。这次,使用名为 popup 的精灵。
The UI has a button to open a pop-up window, but there’s no pop-up yet. That will be a new image object, along with several controls (such as buttons and sliders) attached to that object. The first step is to create a new image, so choose GameObject > UI > Image. Just as before, the new image has a slot in the Inspector called Source Image. Drag a sprite to that slot to set this image. This time, use the sprite called popup.
通常,精灵会拉伸到整个图像对象上;这就是乐谱和装备图像的工作方式,单击“设置原始大小”按钮可将对象调整为图像大小。此行为是图像对象的默认行为,但弹出窗口将使用切片图像。
Ordinarily, the sprite is stretched over the entire image object; this was how the score and gear images worked, and you clicked the Set Native Size button to resize the object to the size of the image. This behavior is the default for image objects, but the pop-up will use a sliced image instead.
定义切片图像被分成九个部分,每个部分都以不同的方式缩放。通过分别缩放图像的边缘和中间部分,可以确保图像可以缩放到您想要的任何大小,并且保持其锐利、清晰的边缘。在其他开发工具中,这类图像的名称中通常会有“9”(例如 9-slice、9-patch、scale-9),以表示图像的九个部分。
DEFINITION A sliced image is split into nine sections that scale differently from one another. By scaling the edges of the image separately from the middle, you ensure that the image can scale to any size you want, and it maintains its sharp, crisp edges. In other development tools, these kinds of images often have “9” somewhere in the name (such as 9-slice, 9-patch, scale-9) to indicate the nine sections of the image.
如图 7.13 所示,图像组件有一个图像类型设置。此设置默认为 Simple,这是之前正确的图像类型。但是,对于弹出窗口,请将图像类型设置为 Sliced。Unity 可能会显示错误,抱怨图像没有边框,因此我们接下来将纠正它。
As you can see in figure 7.13, the image component has an Image Type setting. This setting defaults to Simple, which was the correct image type earlier. For the pop-up, though, set Image Type to Sliced. Unity will probably display an error, complaining that the image doesn’t have a border, so we’ll correct that next.
Figure 7.13 Settings for the image component, including Image Type
发生错误是因为弹出精灵尚未定义九个边框部分。要进行设置,首先在 Project 视图中选择弹出精灵。在 Inspector 中,您应该会看到 Sprite Editor 按钮(见图 7.14);单击该按钮,就会出现 Sprite Editor 窗口。
The error happens because the popup sprite doesn’t have the nine border sections defined yet. To set that up, first select the popup sprite in the Project view. In the Inspector, you should see the Sprite Editor button (see figure 7.14); click that button, and the Sprite Editor window will appear.
警告如第 6 章所述,Sprite Editor 窗口需要 2D Sprite 包。创建 2D 项目可能会自动安装该包,但对于此项目,您需要打开 Window > Package Manager 并在窗口左侧的列表中查找 2D Sprite。选择该包,然后单击 Install 按钮。
WARNING As mentioned in chapter 6, the Sprite Editor window requires the 2D Sprite package. Creating a 2D project may automatically install that package, but for this project, you need to open Window > Package Manager and look for 2D Sprite in the list on the left side of the window. Select that package and then click the Install button.
图 7.14 检查器中的 Sprite Editor 按钮和弹出窗口
Figure 7.14 Sprite Editor button in the Inspector and a pop-up window
在 Sprite 编辑器中,您可以看到绿线,指示图像的切片方式。最初,图像没有任何边框(所有边框设置均为 0)。将所有四边的边框宽度增加到12,这将产生如图 7.14 所示的边框。由于所有四边(左、右、底部和顶部)的边框宽度都设置为 12 像素,因此边框线将相交成九个部分。关闭编辑器窗口并应用更改。
In the Sprite Editor, you can see green lines that indicate how the image will be sliced. Initially, the image won’t have any border (all of the Border settings are 0). Increase the border width to 12 for all four sides, which will result in the border shown in figure 7.14. Because all four sides (Left, Right, Bottom, and Top) have the border set to 12 pixels wide, the border lines will intersect into nine sections. Close the editor window and apply the changes.
现在精灵已经定义了九个部分,切片图像将正常工作(图像组件设置将显示填充中心;确保该设置已打开)。单击并拖动图像角落的蓝色指示器以缩放它(如果您没有看到任何缩放指示器,请切换到第 5 章中描述的矩形工具)。边框部分将保持其大小,而中心部分将缩放。
Now that the sprite has the nine sections defined, the sliced image will work correctly (and the Image component settings will show Fill Center; make sure that setting is on). Click and drag the blue indicators in the corner of the image to scale it (switch to the Rect tool described in chapter 5 if you don’t see any scale indicators). The border sections will maintain their size while the center portion scales.
由于边框部分保持其大小,因此切片图像可以缩放到任何大小,并且仍然具有清晰的边缘。这对于 UI 元素来说非常完美——不同的窗口可能大小不同,但看起来仍然应该相同。对于此弹出窗口,输入宽度250和高度200,使其看起来像图 7.15(此外,将其置于位置0,0,0的中心)。
Because the border sections maintain their size, a sliced image can be scaled to any size and still have crisp edges. This is perfect for UI elements—different windows may be different sizes but should still look the same. For this pop-up, enter a width of 250 and a height of 200 to make it look like figure 7.15 (also, center it on position 0, 0, 0).
Figure 7.15 Sliced image scaled to dimensions of the pop-up
提示UI 图像堆叠的方式由其在 Hierarchy 视图中的顺序决定。在 Hierarchy 列表中,将弹出对象拖到其他 UI 对象上方(当然,始终保持与画布的连接)。现在在 Scene 视图中移动弹出窗口;您可以看到图像如何与弹出窗口重叠。最后,将弹出窗口拖到画布层次结构的底部,以便它显示在所有其他对象的顶部。
TIP The way that UI images stack on top of each other is determined by their order in the Hierarchy view. In the Hierarchy list, drag the pop-up object above other UI objects (always staying attached to the canvas, of course). Now move the pop-up around within the Scene view; you can see how images overlap the pop-up window. Finally, drag the pop-up to the bottom of the canvas hierarchy so that it will display on top of everything else.
弹出对象现已设置完毕,因此请为其编写一些代码。创建一个名为SettingsPopup的脚本并将该脚本拖到弹出对象上。
The pop-up object is set up now, so write some code for it. Create a script called SettingsPopup and drag that script onto the pop-up object.
Listing 7.4 SettingsPopup script for the pop-up object
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 SettingsPopup:MonoBehaviour {
公共无效打开(){
gameObject.SetActive(true); ❶
}
公共无效关闭(){
gameObject.SetActive(false); ❷
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SettingsPopup : MonoBehaviour {
public void Open() {
gameObject.SetActive(true); ❶
}
public void Close() {
gameObject.SetActive(false); ❷
}
}
❶ Turn the object on to open the window.
❷ Deactivate this object to close the window.
Next, open UIController to make a few adjustments.
Listing 7.5 Adjusting UIController to handle the pop-up
...
[SerializeField] 设置弹出设置弹出;
无效开始(){
settingsPopup.Close(); ❶
}
...
公共无效OnOpenSettings(){
settingsPopup.Open(); ❷
}
......
[SerializeField] SettingsPopup settingsPopup;
void Start() {
settingsPopup.Close(); ❶
}
...
public void OnOpenSettings() {
settingsPopup.Open(); ❷
}
...
❶ Close the pop-up when the game starts.
❷ Replace the debug text with the pop-up’s method.
此代码为弹出窗口对象添加了一个插槽,因此将弹出窗口拖到UIController。 玩游戏时,弹出窗口最初会关闭,单击“设置”按钮时会打开。
This code adds a slot for the pop-up object, so drag the pop-up to UIController. The pop-up will be closed initially when you play the game, and it’ll open when you click the Settings button.
目前,没有办法再次关闭它,因此向弹出窗口添加一个按钮。步骤与之前创建的按钮基本相同:选择 GameObject > UI> Button - TextMeshPro,将新按钮定位到弹出窗口的右上角,将关闭精灵拖到此 UI 元素的 Source Image 属性上,然后单击 Set Native Size 以正确调整图像大小。与上一个按钮不同,我们需要这个文本标签,因此选择文本对象并在文本字段中输入Close ,将 Font Size 减小到14 ,并将 Vertex Color 设置为白色。在 Hierarchy 视图中,将此按钮拖到弹出窗口对象上,使其成为弹出窗口的子对象。最后,将按钮过渡调整为 Fade Duration 值0.01和较暗的 Normal Color 设置210、210、210、255。
At the moment, there’s no way to close it again, so add a button to the pop-up. The steps are pretty much the same as for the button created earlier: choose GameObject > UI> Button - TextMeshPro, position the new button in the top-right corner of the pop-up, drag the close sprite to this UI element’s Source Image property, and then click Set Native Size to correctly resize the image. Unlike with the previous button, we want this text label, so select the text object and type Close in the text field, reduce Font Size to 14, and set Vertex Color to white. In the Hierarchy view, drag this button onto the pop-up object so that it will be a child of the pop-up window. And as a final touch of polish, adjust the button transition to a Fade Duration value of 0.01 and a darker Normal Color setting of 210, 210, 210, 255.
要使按钮关闭弹出窗口,需要一个OnClick条目;单击按钮的 On Click 面板上的 + 按钮,将弹出窗口拖到对象槽中,然后从函数列表中选择SettingsPopup > Close()。现在玩游戏,这个按钮将关闭弹出窗口。
To make the button close the pop-up, it needs an OnClick entry; click the + button on the button’s On Click panel, drag the pop-up window into the object slot, and choose SettingsPopup > Close() from the function list. Now play the game, and this button will close the pop-up window.
弹出窗口已添加到 HUD。不过,窗口目前是空白的,因此让我们添加控件来它。
The pop-up window has been added to the HUD. The window is currently blank, though, so let’s add controls to it.
作为使用我们之前制作的按钮,向设置弹出窗口添加控件涉及两个主要步骤。创建附加到画布的 UI 元素,并将这些对象链接到脚本。我们需要的输入控件是文本字段和滑块,以及用于标识滑块的静态文本标签。选择 GameObject > UI > InputField - TextMeshPro 以创建文本字段,选择 GameObject > UI > Slider 以创建滑块对象,选择 GameObject > UI > Text - TextMeshPro 以创建文本标签对象(见图 7.16)。
As with the buttons we made earlier, adding controls to the settings pop-up involves two main steps. You create UI elements attached to the canvas and link those objects to a script. The input controls we need are a text field and a slider, as well as a static text label to identify the slider. Choose GameObject > UI > InputField - TextMeshPro to create the text field, GameObject > UI > Slider to create the slider object, and GameObject > UI > Text - TextMeshPro to create the text label object (see figure 7.16).
Figure 7.16 Input controls added to the pop-up window
通过在层次结构视图中拖动所有三个对象,使它们成为弹出窗口的子对象,然后按照图中所示放置它们,在弹出窗口的中间排成一排。要为滑块制作标签,请将文本对象设置为Speed并将其涂成黑色。输入字段用于输入文本,大文本框的内容会在玩家输入其他内容之前显示;将此值设置为Name。您可以将选项 Content Type 和 Line Type 保留为默认值;如果需要,您可以使用 Content Type 将输入限制为仅字母或仅数字等内容,而您可以使用 Line Type 从单行切换到多行文本。
Make all three objects children of the pop-up by dragging them in the Hierarchy view and then position them as indicated in the figure, lined up in the middle of the pop-up. To make a label for the slider, set the text object to Speed and color it black. The input field is for typing in text, and the content of the big text box is shown before the player types something else; set this value to Name. You can leave the options Content Type and Line Type at their defaults; if desired, you can use Content Type to restrict typing to things like only letters or only numbers, whereas you can use Line Type to switch from a single line to multiline text.
警告:如果文本标签覆盖了滑块,您将无法点击滑块。将文本对象移到层次结构中的滑块上方,或者最好关闭 Raycast Target 设置(展开 Extra Settings,如图 7.7 所示),这样鼠标点击就会忽略此对象。
WARNING You won’t be able to click the slider if the text label covers it. Move the text object above the slider in the Hierarchy, or better yet turn off the Raycast Target setting (expand Extra Settings as shown in figure 7.7) so that mouse clicks will ignore this object.
警告:在本例中,您可能应该保留输入字段的默认大小,但如果您决定缩小它,请仅减少宽度,而不是高度。如果将高度设置为小于 30,则文本太小而无法显示。
WARNING You should probably leave the Input Field at the default size for this example, but if you do decide to shrink it, reduce only the Width, not the Height. If you set the Height to less than 30, that’s too small for the text to appear.
至于滑块本身,组件检查器底部会出现几个设置。最小值默认设置为 0;保留该值。最大值默认为 1,但在本例中将其设置为2。同样,值和整数都可以保留其默认值;值控制滑块的起始值,整数将其限制为 0、1、2,而不是小数值(我们不想要的限制)。
As for the slider itself, several settings appear toward the bottom of the component inspector. Min Value is set to 0 by default; leave that. Max Value defaults to 1, but make it 2 for this example. Similarly, both Value and Whole Numbers can be left at their defaults; Value controls the starting value of the slider, and Whole Numbers constrains it to 0, 1, 2 rather than decimal values (a constraint we don’t want).
这样就包装好了所有对象。现在您需要编写对象所链接的代码;将以下清单中显示的方法添加到SettingsPopup中。
And that wraps up all the objects. Now you need to write the code that the objects are linked to; add the methods shown in the following listing to SettingsPopup.
清单 7.6弹出窗口输入控件的SettingsPopup方法
Listing 7.6 SettingsPopup methods for the pop-up’s input controls
...
public void OnSubmitName(string name) { ❶
调试.日志(名称);
}
public void OnSpeedValue(float speed) { ❷
Debug.Log($"速度:{speed}"); ❸
}
......
public void OnSubmitName(string name) { ❶
Debug.Log(name);
}
public void OnSpeedValue(float speed) { ❷
Debug.Log($"Speed: {speed}"); ❸
}
...
❶ Triggers when the user types in the input field
❷ Triggers when the user adjusts the slider
❸ Constructs the message using string interpolation
太棒了!我们有了可供控件使用的方法。现在选择输入对象,在设置底部,您将看到一个 On End Edit 面板;当用户完成输入时,将触发此处列出的事件。向此面板添加一个条目,将弹出窗口拖到对象槽,然后在函数列表中选择SettingsPopup.OnSubmitName() 。
Great! We have methods for the controls to use. Now select the input object, and at the bottom of the settings you’ll see an On End Edit panel; events listed here are triggered when the user finishes typing. Add an entry to this panel, drag the pop-up to the object slot, and choose SettingsPopup.OnSubmitName() in the function list.
警告确保选择 End Edit 面板顶部的函数“动态字符串”,而不是底部的“静态参数”。OnSubmitName ()函数出现在两个部分中,但在“静态参数”下选择它将仅发送提前定义的单个字符串;动态字符串指的是输入字段中输入的任何值。
WARNING Be sure to select the function in the End Edit panel’s top section, Dynamic String, and not the bottom section, Static Parameters. The OnSubmitName() function appears in both sections, but selecting it under Static Parameters will send only a single string defined ahead of time; dynamic string refers to whatever value is typed in the input field.
对滑块执行相同的步骤:查找组件设置末尾的事件面板(在本例中,面板为 OnValueChanged),单击 + 添加条目,拖入设置弹出窗口,然后在动态值函数列表中选择SettingsPopup.OnSpeedValue() 。
Follow these same steps for the slider: look for the event panel toward the end of the component settings (in this case, the panel is OnValueChanged), click + to add an entry, drag in the settings pop-up, and choose SettingsPopup.OnSpeedValue() in the list of dynamic value functions.
现在,两个输入控件都已连接到弹出脚本中的代码。玩游戏,并在移动滑块或在输入输入后按 Enter 时观察控制台。
Now both of the input controls are connected to code in the pop-up’s script. Play the game, and watch the console while you move the slider or press Enter after typing input.
虽然控件会生成调试输出,但它们仍然不会影响游戏。让 HUD 影响游戏(反之亦然)是这章。
Although the controls generate debug output, they still don’t affect the game. Making the HUD affect the game (and vice versa) is the topic of the final section of this chapter.
向上到目前为止,HUD 和主游戏一直相互忽略,但它们应该相互通信。这可以通过脚本引用来实现,就像您对其他类型的对象间通信所做的那样,但这种方法有很大的缺点。特别是,这样做会将场景和 HUD 紧密耦合;您希望让它们彼此保持相当的独立性,这样您就可以自由地编辑游戏而不必担心破坏了 HUD。
Up to now, the HUD and main game have been ignoring each other, but they ought to be communicating back and forth. That could be accomplished via script references, as you’ve done for other sorts of inter-object communication, but that approach would have major downsides. In particular, doing so would tightly couple the scene and the HUD; you want to keep them fairly independent of each other so that you can freely edit the game without worrying that you’ve broken the HUD.
为了向 UI 发出场景中操作的警报,我们将使用广播消息系统。图 7.17 说明了此事件消息系统的工作原理:脚本可以注册以监听事件,其他代码可以广播事件,并且监听器将收到有关广播消息的警报。让我们介绍一个消息系统来实现这一点。
To alert the UI of actions in the scene, we’re going to use a broadcast messenger system. Figure 7.17 illustrates how this event messaging system works: scripts can register to listen for an event, other code can broadcast an event, and listeners will be alerted about broadcast messages. Let’s go over a messaging system to accomplish that.
Figure 7.17 Diagram of the broadcast event system we’ll implement
提示C# 确实有一个用于处理事件的内置系统,所以您可能想知道为什么我们不使用它。好吧,内置事件系统强制执行目标消息,而我们需要一个广播消息系统。目标系统要求代码确切知道消息来自何处;广播可以来自任何地方。
TIP C# does have a built-in system for handling events, so you might wonder why we don’t use that. Well, the built-in event system enforces targeted messages, whereas we want a broadcast messenger system. A targeted system requires the code to know exactly where messages originate from; broadcasts can originate from anywhere.
到提醒 UI 场景中的操作,我们将使用广播信使系统。虽然 Unity 没有内置此功能,但您可以为此下载一个好的代码库。此信使系统非常适合提供一种将事件传达给程序其余部分的解耦方式。当某些代码广播消息时,该代码不需要知道有关侦听器的任何信息,从而允许在切换或添加对象时具有很大的灵活性。
To alert the UI of actions in the scene, we’re going to use a broadcast messenger system. Although Unity doesn’t have this feature built in, you can download a good code library for this purpose. This messenger system is great for providing a decoupled way of communicating events to the rest of the program. When some code broadcasts a message, that code doesn’t need to know anything about the listeners, allowing for a great deal of flexibility in switching around or adding objects.
创建一个名为Messenger的脚本并粘贴来自https://github.com/jhocking/from-unity-wiki/blob/main/Messenger.cs的代码。然后,您还需要创建一个名为GameEvent 的脚本并用清单 7.7 中的代码填充它。
Create a script called Messenger and paste in the code from https://github.com/jhocking/from-unity-wiki/blob/main/Messenger.cs. Then, you also need to create a script called GameEvent and fill it with the code from listing 7.7.
清单 7.7与Messenger一起使用的GameEvent脚本
Listing 7.7 GameEvent script to use with Messenger
公共静态类 GameEvent {
公共 const 字符串 ENEMY_HIT = "ENEMY_HIT";
公共 const 字符串 SPEED_CHANGED = “SPEED_CHANGED”;
}public static class GameEvent {
public const string ENEMY_HIT = "ENEMY_HIT";
public const string SPEED_CHANGED = "SPEED_CHANGED";
}
该脚本定义了一些事件消息的常量;通过这种方式,消息更有条理,您不必记住并在各处输入消息字符串。
This script defines constants for a couple of event messages; the messages are more organized this way, and you don’t have to remember and type the message string all over the place.
现在事件消息系统已准备就绪,让我们开始使用它。首先,我们将从场景与 HUD 进行通信,然后进入另一个方向。
Now the event messenger system is ready to use, so let’s start using it. First, we’ll communicate from the scene to the HUD, and then we’ll go in the other direction.
向上到目前为止,分数显示已经显示了一个计时器,作为文本显示功能的测试。但我们想显示被击中的敌人数量,所以让我们修改UIController中的代码。首先,删除整个Update()方法,因为这是测试代码。当敌人死亡时,它会发出一个事件,因此下面的清单使UIController监听该事件。
Up to now, the score display has displayed a timer as a test of the text display functionality. But we want to display a count of enemies hit, so let’s modify the code in UIController. First, delete the entire Update() method, because that was the test code. When an enemy dies, it will emit an event, so the following listing makes UIController listen for that event.
Listing 7.8 Adding event listeners to UIController
...
私人 int 分数;
无效OnEnable(){
Messenger.AddListener(GameEvent.ENEMY_HIT,OnEnemyHit); ❶
}
无效OnDisable(){
Messenger.RemoveListener(GameEvent.ENEMY_HIT,OnEnemyHit); ❷
}
无效开始(){
分数=0;
分数标签.文本 = 分数.ToString(); ❸
设置弹出.关闭();
}
私有 void OnEnemyHit() {
分数 += 1; ❹
分数标签.文本 = 分数.ToString();
}
......
private int score;
void OnEnable() {
Messenger.AddListener(GameEvent.ENEMY_HIT, OnEnemyHit); ❶
}
void OnDisable() {
Messenger.RemoveListener(GameEvent.ENEMY_HIT, OnEnemyHit); ❷
}
void Start() {
score = 0;
scoreLabel.text = score.ToString(); ❸
settingsPopup.Close();
}
private void OnEnemyHit() {
score += 1; ❹
scoreLabel.text = score.ToString();
}
...
❶ Declare which method responds to the ENEMY_HIT event.
❷ When an object is deactivated, remove the listener to avoid errors.
❹ Increment the score in response to the event.
首先注意OnEnable()和OnDisable()方法。与Start()和Update()非常相似,每个MonoBehaviour都会在对象被激活或停用时自动响应。侦听器在OnEnable() / OnDisable()中添加和删除。此侦听器是广播消息系统的一部分,当收到该消息时,它会调用OnEnemyHit() 。 OnEnemyHit()会增加分数,然后将该值放入分数显示中。
First notice the OnEnable() and OnDisable() methods. Much like Start() and Update(), every MonoBehaviour automatically responds when the object is activated or deactivated. A listener gets added and removed in OnEnable()/OnDisable(). This listener is part of the broadcast messaging system, and it calls OnEnemyHit() when that message is received. OnEnemyHit()increments the score and then puts that value in the score display.
事件侦听器是在 UI 代码中设置的,因此现在我们需要在敌人被击中时广播该消息。响应击中的代码位于RayShooter中,因此请按此处所示发出消息。
The event listeners are set up in the UI code, so now we need to broadcast that message whenever an enemy is hit. The code to respond to hits is in RayShooter, so emit the message as shown here.
Listing 7.9 Broadcast event message from RayShooter
...
如果 (目标 != 空) {
目标.ReactToHit();
Messenger.Broadcast(GameEvent.ENEMY_HIT); ❶
} 别的 {
......
if (target != null) {
target.ReactToHit();
Messenger.Broadcast(GameEvent.ENEMY_HIT); ❶
} else {
...
❶ Message broadcast added to hit response
添加该消息后玩游戏,并在射击敌人时观察分数显示。每次击中敌人时,你应该看到计数增加。这涵盖了从 3D 游戏向 2D 界面发送消息,但我们还需要一个示例方向。
Play the game after adding that message and watch the score display when you shoot an enemy. You should see the count going up every time you make a hit. That covers sending messages from the 3D game to the 2D interface, but we also want an example going in the other direction.
在在上一节中,一个事件从场景中广播并由 HUD 接收。以类似的方式,UI 控件可以广播玩家和敌人都会听到的消息。通过这种方式,设置弹出窗口可以影响游戏的设置。打开WanderingAI并添加此代码。
In the previous section, an event was broadcast from the scene and received by the HUD. In a similar way, UI controls can broadcast a message that both players and enemies listen for. In this way, the settings pop-up can affect the settings of the game. Open WanderingAI and add this code.
Listing 7.10 Event listener added to WanderingAI
... 公共 const float baseSpeed = 3.0f; ❶ ... 无效OnEnable(){ Messenger<float>.AddListener(GameEvent.SPEED_CHANGED,OnSpeedChanged); } 无效OnDisable(){ Messenger<float>.RemoveListener(GameEvent.SPEED_CHANGED,OnSpeedChanged); } ... 私有 void OnSpeedChanged(float 值) { ❷ 速度 = 基本速度 * 值; } ...
... public const float baseSpeed = 3.0f; ❶ ... void OnEnable() { Messenger<float>.AddListener(GameEvent.SPEED_CHANGED, OnSpeedChanged); } void OnDisable() { Messenger<float>.RemoveListener(GameEvent.SPEED_CHANGED, OnSpeedChanged); } ... private void OnSpeedChanged(float value) { ❷ speed = baseSpeed * value; } ...
❶ Base speed that is adjusted by the speed setting
❷ Method that was declared in listener for event SPEED_CHANGED
OnEnable()和OnDisable()也分别在这里添加和删除事件侦听器,但这次这些方法有一个值。该值用于设置漫游 AI 的速度。
OnEnable() and OnDisable() add and remove, respectively, an event listener here, too, but the methods have a value this time. That value is used to set the speed of the wandering AI.
提示上一节中的代码使用了通用事件,但此消息系统还可以随消息传递值。在侦听器中支持值就像添加类型定义一样简单;请注意侦听器命令中添加的<float> 。
TIP The code in the previous section used a generic event, but this messaging system can also pass a value along with the message. Supporting a value in the listener is as simple as adding a type definition; note the <float> added to the listener command.
现在对FPSInput进行同样的更改以影响播放器的速度。下一个清单中的代码与清单 7.10 中的代码几乎相同,只是播放器的baseSpeed数值不同。
Now make the same changes in FPSInput to affect the speed of the player. The code in the next listing is almost the same as that in listing 7.10, except that the player has a different number for baseSpeed.
Listing 7.11 Event listener added to FPSInput
...
公共 const float baseSpeed = 6.0f; ❶
...
无效OnEnable(){
Messenger<float>.AddListener(GameEvent.SPEED_CHANGED,OnSpeedChanged);
}
无效OnDisable(){
Messenger<float>.RemoveListener(GameEvent.SPEED_CHANGED,OnSpeedChanged);
}
...
私有 void OnSpeedChanged(浮点值) {
速度 = 基本速度 * 值;
}
......
public const float baseSpeed = 6.0f; ❶
...
void OnEnable() {
Messenger<float>.AddListener(GameEvent.SPEED_CHANGED, OnSpeedChanged);
}
void OnDisable() {
Messenger<float>.RemoveListener(GameEvent.SPEED_CHANGED, OnSpeedChanged);
}
...
private void OnSpeedChanged(float value) {
speed = baseSpeed * value;
}
...
❶ This value is changed from listing 7.10.
Finally, broadcast the speed values from SettingsPopup in response to the slider.
Listing 7.12 Broadcast message from SettingsPopup
公共无效OnSpeedValue(浮动速度){
Messenger<float>.Broadcast(GameEvent.SPEED_CHANGED, speed); ❶
...public void OnSpeedValue(float speed) {
Messenger<float>.Broadcast(GameEvent.SPEED_CHANGED, speed); ❶
...
❶ Send slider value as <float> event.
现在,当你调整滑块时,敌人和玩家的速度都会改变。单击“播放”并尝试一下!
Now the enemy and player have their speed changed when you adjust the slider. Click Play and try it out!
现在,您知道如何使用 Unity 提供的新 UI 工具构建图形界面。这些知识将在所有未来的项目中派上用场,即使我们探索不同的游戏類型。
You now know how to build a graphical interface by using the new UI tools offered by Unity. This knowledge will come in handy in all future projects, even as we explore different game genres.
Unity has both an immediate mode GUI system as well as a newer system based on 2D sprites.
Using 2D sprites for a GUI requires that the scene have a canvas object.
UI elements can be anchored to relative positions on the adjustable canvas.
A decoupled messaging system is a great way to broadcast events between the interface and the scene.
在本章中,您将创建另一个 3D 游戏,但这次您将从事一种新的游戏类型。在第 2 章中,您为第一人称游戏构建了一个移动演示。现在您将编写另一个移动演示,但这次它将涉及第三人称移动。最重要的区别是相机相对于玩家的位置:玩家在第一人称视角中通过角色的眼睛看东西,而在第三人称视角中相机位于角色之外。您可能从冒险游戏中熟悉这种视图,例如经久不衰的《塞尔达传说》系列或较新的《神秘海域》系列。(如果您想查看第一人称和第三人称视图的比较,请跳到图 8.3。)
In this chapter, you’ll create another 3D game, but this time you’ll be working in a new game genre. In chapter 2, you built a movement demo for a first-person game. Now you’re going to write another movement demo, but this time it’ll involve third-person movement. The most important difference is the placement of the camera relative to the player: a player sees through their character’s eyes in first-person view, and the camera is placed outside the character in third-person view. This view is probably familiar to you from adventure games, like the long-lived Legend of Zelda series or the more recent Uncharted series. (Skip ahead to figure 8.3 if you want to see a comparison of first-person and third-person views.)
本章中的项目是本书中我们将构建的视觉效果更令人兴奋的原型之一。图 8.1 显示了场景的构建方式。将其与我们在第 2 章中创建的第一人称场景的图表(图 2.2)进行比较。
The project in this chapter is one of the more visually exciting prototypes we’ll build in this book. Figure 8.1 shows how the scene will be constructed. Compare this with the diagram of the first-person scene we created in chapter 2 (figure 2.2).
Figure 8.1 Road map for the third-person movement demo
您可以看到房间结构相同,脚本的使用也大体相同。但玩家的外观以及摄像头的位置在每种情况下都不同。同样,第三人称视角的定义是摄像头位于玩家角色之外并向内看该角色。您将使用看起来像人形角色的模型(而不是原始胶囊),因为现在玩家可以真正看到自己。
You can see that the room construction is the same, and the use of scripts is much the same. But the look of the player, as well as the placement of the camera, are different in each case. Again, what defines this as a third-person view is that the camera is outside the player’s character and looking inward at that character. You’ll use a model that looks like a humanoid character (rather than a primitive capsule) because now players can actually see themselves.
回想一下,第 4 章讨论的两种艺术资产类型是 3D 模型和动画。如前几章所述,术语3D 模型几乎是网格对象的同义词;3D 模型是由顶点和多边形定义的静态形状(即网格几何体)。对于人形角色,此网格几何体被塑造成头部、手臂、腿部等(见图 8.2)。
Recall that two of the types of art assets discussed in chapter 4 were 3D models and animations. As mentioned in earlier chapters, the term 3D model is almost a synonym for mesh object; the 3D model is the static shape defined by vertices and polygons (that is, mesh geometry). For a humanoid character, this mesh geometry is shaped into a head, arms, legs, and so forth (see figure 8.2).
Figure 8.2 Wireframe view of the model we’ll use in this chapter
与往常一样,我们将重点关注路线图的最后一步:对场景中的对象进行编程。以下是我们的行动计划的回顾:
As usual, we’ll focus on the last step in the road map: programming objects in the scene. Here’s a recap of our plan of action:
复制第 2 章中的项目进行修改,或者创建一个新的 Unity 项目(确保将其设置为 3D,而不是第 5 章中的 2D 项目)并复制第 2 章项目中的场景文件。无论哪种方式,还可以从本章的下载中获取临时文件夹以获取我们将使用的角色模型。
Copy the project from chapter 2 to modify it, or create a new Unity project (be sure it’s set to 3D, not the 2D project from chapter 5) and copy over the scene file from chapter 2’s project. Either way, also grab the scratch folder from this chapter’s download to get the character model we’ll use.
注意您将在第 2 章的围墙区域中构建本章的项目。您将保留墙壁和灯光,但替换播放器和所有脚本。如果您需要示例文件,请从该章下载它们。
NOTE You’re going to build this chapter’s project in the walled area from chapter 2. You’ll keep the walls and lights but replace the player and all the scripts. If you need the sample files, download them from that chapter.
假设您从第 2 章的已完成项目开始(移动演示,而不是后续项目),让我们删除本章不需要的所有内容。首先,在层次结构列表中断开相机与玩家的连接(将相机对象从玩家对象上拖出)。现在删除玩家对象;如果您没有先断开相机连接,它也会被删除,但您想要的是只删除玩家胶囊并保留相机。或者,如果您已经意外删除了相机,请通过选择 GameObject > Camera 创建一个新相机对象。
Assuming you’re starting with the completed project from chapter 2 (the movement demo, not later projects), let’s delete everything we don’t need for this chapter. First, disconnect the camera from the player in the Hierarchy list (drag the camera object off the player object). Now delete the player object; if you hadn’t disconnected the camera first, that would be deleted too, but what you want is to delete only the player capsule and leave the camera. Alternatively, if you already deleted the camera by accident, create a new camera object by choosing GameObject > Camera.
删除所有脚本(包括从相机中删除脚本组件并删除项目视图中的文件),只留下墙壁、地板和灯光。
Delete all the scripts as well (which involves removing the script component from the camera and deleting the files in the Project view), leaving only the walls, floor, and lights.
前您可以编写代码让玩家四处移动,您需要在场景中放置一个角色并设置相机来观察该角色。您将导入一个无脸人形模型作为玩家角色,然后将相机以一定角度放置在上方以斜视玩家。图 8.3 比较了第一人称视角下的场景与第三人称视角下的场景(用几个大块显示,您将在本章中添加)。您已经准备好了场景,现在您将把角色模型放入场景中。
Before you can write code to make the player move around, you need to put a character in the scene and set up the camera to look at that character. You’ll import a faceless humanoid model to use as the player character, and then place the camera above at an angle to look down at the player obliquely. Figure 8.3 compares what the scene looks like in first-person view with what the scene will look like in third-person view (shown with a few large blocks, which you’ll add in this chapter). You’ve prepared the scene already, so now you’ll put a character model into the scene.
Figure 8.3 Side-by-side comparison of first-person and third-person views
这本章下载的临时文件夹包含模型和纹理。正如您从第 4 章中回忆的那样,FBX 是模型,TGA 是纹理。将 FBX 文件导入项目:将文件拖到项目视图中,或在项目视图中右键单击并选择导入新资产。
The scratch folder for this chapter’s download includes both the model and the texture. As you’ll recall from chapter 4, FBX is the model, and TGA is the texture. Import the FBX file into the project: either drag the file into the Project view, or right-click in the Project view and select Import New Asset.
然后查看检查器以调整模型的导入设置。本章后面的内容中,您将调整导入的动画,但现在,您只需要在“模型”和“材料”选项卡中进行一些调整。首先,转到“模型”选项卡并将“比例因子”值更改为10(以部分抵消“转换单位”值 0.01),以便模型具有正确的大小。
Then look in the Inspector to adjust import settings for the model. Later in the chapter, you’ll adjust imported animations, but for now, you need to make only a couple of adjustments in the Model and Materials tabs. First, go to the Model tab and change the Scale Factor value to 10 (to partially counteract the Convert Units value of 0.01) so that the model will be the correct size.
再往下一点,你会看到“法线”选项(见图 8.4)。此设置使用称为“法线”的 3D 数学概念来控制模型上的光照和阴影的显示方式。
A bit farther down, you’ll find the Normals option (see figure 8.4). This setting controls how lighting and shading appear on the model, using a 3D math concept known as, well, normals.
Figure 8.4 Import settings for the character model
定义 法线是多边形外凸出的方向向量,用于告诉计算机多边形朝向哪个方向。此朝向用于照明计算。
DEFINITION Normals are direction vectors sticking out of polygons that tell the computer which direction the polygon is facing. This facing direction is used for lighting calculations.
法线的默认设置是导入,它将使用导入的网格几何体中定义的法线。但是这个特定模型没有正确定义的法线,并且对光线的反应很奇怪。相反,将设置更改为计算,以便 Unity 为每个多边形的朝向计算一个矢量。调整这些设置后,单击检查器中的应用按钮。
The default setting for Normals is Import, which will use the normals defined in the imported mesh geometry. But this particular model doesn’t have correctly defined normals and will react in odd ways to lights. Instead, change the setting to Calculate so that Unity will calculate a vector for the facing direction of every polygon. Once you’ve adjusted these settings, click the Apply button in the Inspector.
接下来,将 TGA 文件导入项目(以便将此图像指定为玩家材质的纹理)。转到“材质”选项卡并单击“提取材质”按钮。提取到您想要的任何位置;然后选择出现的材质并将纹理图像拖到 Inspector 中的 Albedo 纹理槽中。应用纹理后,您将不会看到模型颜色发生显著变化(此纹理图像大部分为白色),但绘制到纹理中的阴影将改善模型的外观。
Next, import the TGA file into the project (in order to assign this image as the texture on the player’s material). Go to the Materials tab and click the Extract Materials button. Extract to whatever location you feel like; then select the material that appeared and drag the texture image onto the Albedo texture slot in the Inspector. Once the texture is applied, you won’t see a dramatic change in the model’s color (this texture image is mostly white), but shadows that are painted into the texture will improve the look of the model.
应用纹理后,将玩家模型从项目视图拖到场景中。将角色定位在0 , 1.1 , 0处,使其位于房间中央并抬起以站在地板上。我们在场景!
With the texture applied, drag the player model from the Project view up into the scene. Position the character at 0, 1.1, 0 so that it’ll be in the center of the room and raised up to stand on the floor. We have a third-person character in the scene!
注意:导入的角色手臂伸直,而不是更自然的手臂下垂姿势。这是因为动画尚未应用;手臂伸出的姿势称为T 姿势,并且标准是动画角色在动画之前默认采用 T 姿势。
NOTE The imported character has arms stuck straight out to each side, rather than the more natural arms-down pose. That’s because animations haven’t been applied yet; that arms-out position is referred to as the T-pose, and the standard is for animated characters to default to a T-pose before they’re animated.
前我们继续,我想解释一下角色投射的阴影。在现实世界中,我们认为阴影是理所当然的,但在游戏的虚拟世界中,阴影却不一定存在。幸运的是,Unity 可以处理这个细节,并且阴影会为新场景附带的默认灯光打开。
Before we move on, I want to explain a bit about the shadow being cast by the character. We take shadows for granted in the real world, but shadows aren’t guaranteed in the game’s virtual world. Fortunately, Unity can handle this detail, and shadows are turned on for the default light that comes with new scenes.
选择场景中的定向光,然后在检查器中查找阴影类型选项。该设置(图 8.5)已针对默认光启用了软阴影,但请注意,菜单上还有一个无阴影选项。
Select the directional light in your scene and then look in the Inspector for the Shadow Type option. That setting (figure 8.5) is already on Soft Shadows for the default light, but notice that the menu also has a No Shadows option.
Figure 8.5 Before and after casting shadows from the directional light
这就是你在本项目中设置阴影所需要做的全部工作,但你还需要了解有关游戏中阴影的更多信息。计算场景中的阴影是计算机图形学中特别耗时的部分,因此游戏通常会偷工减料并以各种方式伪造事物以实现所需的视觉效果。
That’s all you need to do to set up shadows in this project, but there’s a lot more you should know about shadows in games. Calculating the shadows in a scene is a particularly time-consuming part of computer graphics, so games often cut corners and fake things in various ways to achieve the visual look desired.
角色投射的阴影被称为实时阴影,因为阴影是在游戏运行时计算的,并随移动物体移动。一个完美的逼真照明设置会让所有物体实时投射和接收阴影,但是为了使阴影计算运行得足够快,实时阴影的外观可能很原始,而且游戏甚至可能会限制哪些灯光会投射阴影。请注意,在这个场景中,只有定向光会投射阴影。
The kind of shadow cast from the character is referred to as real-time shadow because the shadow is calculated while the game is running and moves around with moving objects. A perfectly realistic lighting setup would have all objects casting and receiving shadows in real time, but in order for the shadow calculations to run fast enough, the appearance of real-time shadows can be primitive, plus the game may even limit which lights cast shadows. Note that only the directional light is casting shadows in this scene.
Another common way of handling shadows in games is with a technique called lightmapping.
定义 光照贴图是应用于关卡几何的纹理,其中阴影图片被烘焙到纹理图像中。
DEFINITION Lightmaps are textures applied to the level geometry, with pictures of the shadows baked into the texture image.
DEFINITION Drawing shadows onto a model’s texture is referred to as baking the shadows.
由于这些图像是提前生成的(而不是在游戏运行时生成的),因此它们可以非常精致和逼真。缺点是,由于阴影是提前生成的,所以它们不会移动。因此,光照贴图非常适合用于静态关卡几何体,但不适用于角色等动态对象。光照贴图是自动生成的,而不是手工绘制的。计算机计算场景中的灯光将如何照亮关卡,同时在角落中积累微妙的黑暗。
Because these images are generated ahead of time (rather than while the game is running), they can be very elaborate and realistic. On the downside, because the shadows are generated ahead of time, they won’t move. As such, lightmaps are great to use for static-level geometry, but not for dynamic objects like characters. Lightmaps are generated automatically rather than being painted by hand. The computer calculates how the lights in the scene will illuminate the level while subtle darkness builds up in corners.
是否使用实时阴影或光照贴图并不是一个全有或全无的选择。您可以在光源上设置 Culling Mask 属性,以便实时阴影仅用于某些对象,从而允许您对场景中的其他对象使用更高质量的光照贴图。同样,虽然您几乎总是希望主角投射阴影,但有时您不希望角色接收阴影;所有网格对象(在 Mesh Renderer 或 Skinned Mesh Renderer 组件中)都有投射和接收阴影的设置。图 8.6 显示了选择地板时这些设置如何显示。
Whether or not to use real-time shadows or lightmaps isn’t an all-or-nothing choice. You can set the Culling Mask property on a light so that real-time shadows are used only for certain objects, allowing you to use the higher-quality lightmaps for other objects in the scene. Similarly, though you almost always want the main character to cast shadows, sometimes you don’t want the character to receive shadows; all mesh objects (in either Mesh Renderer or Skinned Mesh Renderer components) have settings to cast and receive shadows. Figure 8.6 shows how those settings appear when you select the floor.
Figure 8.6 The Cast Shadows and Receive Shadows settings in the Inspector
定义 剔除是移除不需要的东西的通用术语。这个词在计算机图形学的许多情况下经常出现,但在这种情况下是剔除蒙版是要从阴影投射中移除的对象集。
DEFINITION Culling is a general term for removing unwanted things. The word comes up a lot in computer graphics in many contexts, but in this case culling mask is the set of objects you want to remove from shadow casting.
好了,现在您了解了如何在场景中应用阴影的基础知识。关卡的照明和阴影本身就是一个大话题(关于关卡编辑的书籍通常会用多章来介绍光照贴图),但在这里我们将仅限于在一个光源上打开实时阴影。然后,让我们将注意力转向相机。
All right, now you understand the basics of how to apply shadows to your scenes. Lighting and shading a level can be a big topic in itself (books about level editing will often spend multiple chapters on lightmapping), but here we’ll restrict ourselves to turning on real-time shadows on one light. And with that, let’s turn our attention to the camera.
在在第一人称演示中,相机与层次结构视图中的玩家对象相链接,因此它们会一起旋转。但在第三人称移动中,玩家角色将独立于相机面向不同的方向。因此,这次您不想将相机拖到层次结构视图中的玩家角色上。相反,相机的代码将随角色一起移动其位置,但会独立于角色旋转。
In the first-person demo, the camera was linked to the player object in Hierarchy view so that they’d rotate together. In third-person movement, though, the player character will be facing different directions independently of the camera. Therefore, you don’t want to drag the camera onto the player character in the Hierarchy view this time. Instead, the camera’s code will move its position along with the character but will rotate independently of the character.
首先,将相机放置在您希望它相对于玩家的位置;我选择了位置0、3.5、-3.75 ,将相机放在角色的上方和后方(如果需要,将旋转重置为0、0、0 )。然后创建一个名为OrbitCamera的脚本并编写清单 8.1 中的代码。将脚本组件附加到相机,然后将玩家角色拖到脚本的目标插槽中。现在您可以播放场景以查看相机代码的运行情况。
First, place the camera where you want it to be relative to the player; I went with position 0, 3.5, -3.75 to put the camera above and behind the character (reset rotation to 0, 0, 0 if needed). Then create a script called OrbitCamera and write the code from listing 8.1. Attach the script component to the camera and then drag the player character into the target slot of the script. Now you can play the scene to see the camera code in action.
Listing 8.1 Camera script for rotating around a target while looking at it
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 OrbitCamera : MonoBehaviour {
[SerializeField] 变换目标; ❶
公共浮点旋转速度 = 1.5f;
私有浮动rotY;
私有 Vector3 偏移量;
无效开始(){
rotY = 变换.eulerAngles.y;
偏移量 = 目标.位置 - 变换.位置; ❷
}
无效 LateUpdate() {
float horInput = Input.GetAxis("水平");
如果 (!Mathf.Approximately(horInput, 0)) { ❸
rotY += horInput * rotSpeed;
} 别的 {
rotY += Input.GetAxis("鼠标 X") * rotSpeed * 3; ❹
}
四元数旋转 = 四元数.欧拉(0, rotY, 0);
transform.position = target.position - (旋转 * 偏移); ❺
transform.LookAt(target); ❻
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class OrbitCamera : MonoBehaviour {
[SerializeField] Transform target; ❶
public float rotSpeed = 1.5f;
private float rotY;
private Vector3 offset;
void Start() {
rotY = transform.eulerAngles.y;
offset = target.position - transform.position; ❷
}
void LateUpdate() {
float horInput = Input.GetAxis("Horizontal");
if (!Mathf.Approximately(horInput, 0)) { ❸
rotY += horInput * rotSpeed;
} else {
rotY += Input.GetAxis("Mouse X") * rotSpeed * 3; ❹
}
Quaternion rotation = Quaternion.Euler(0, rotY, 0);
transform.position = target.position - (rotation * offset); ❺
transform.LookAt(target); ❻
}
}
❶ Serialized reference to the object to orbit around
❷ Store the starting position offset between the camera and the target.
❸ Either rotate the camera slowly using arrow keys . . .
❹ . . . or rotate quickly with the mouse.
❺ Maintain the starting offset, shifted according to the camera’s rotation.
❻ No matter where the camera is relative to the target, always face the target.
在阅读清单时,请注意target的序列化变量。代码需要知道摄像机围绕哪个对象旋转,因此此变量被序列化以显示在 Unity 的编辑器中,并将玩家角色链接到它。接下来的几个变量是旋转值,其使用方式与第 2 章中的摄像机控制代码相同。
As you’re reading through the listing, note the serialized variable for target. The code needs to know which object to orbit the camera around, so this variable is serialized to appear within Unity’s editor and have the player character linked to it. The next couple of variables are rotation values that are used in the same way as in the camera control code from chapter 2.
并声明一个偏移值;在Start()中设置偏移量以存储相机和目标之间的位置差。这样,在脚本运行时可以保持相机的相对位置。换句话说,无论相机如何旋转,它都会保持在与角色的初始距离。代码的其余部分位于LateUpdate()函数中。
And an offset value is declared; offset is set within Start() to store the position difference between the camera and target. This way, the relative position of the camera can be maintained while the script runs. In other words, the camera will stay at the initial distance from the character regardless of which way it rotates. The remainder of the code is inside the LateUpdate() function.
提示请记住,LateUpdate()是Mono-Behaviour提供的另一种方法,它与Update()类似;它是每帧运行的方法。顾名思义,区别在于,Update()在所有对象上运行后,才会在所有对象上调用LateUpdate()。这样,您可以确保在目标移动后相机更新。
TIP Remember, LateUpdate() is another method provided by Mono-Behaviour and it’s similar to Update(); it’s a method run every frame. The difference, as the name implies, is that LateUpdate() is called on all objects after Update() has run on all objects. This way, you can ensure that the camera updates after the target has moved.
首先,代码根据输入控件增加旋转值。此代码查看两个输入控件——水平箭头键和水平鼠标移动——因此使用条件在它们之间切换。代码检查是否按下了水平箭头键;如果按下了,则使用该输入,如果没有按下,则检查鼠标。通过分别检查这两个输入,代码可以针对每种类型的输入以不同的速度旋转。
First, the code increments the rotation value based on input controls. This code looks at two input controls—horizontal arrow keys and horizontal mouse movement—so a conditional is used to switch between them. The code checks whether horizontal arrow keys are being pressed; if they are, then it uses that input, but if not, it checks the mouse. By checking the two inputs separately, the code can rotate at different speeds for each type of input.
接下来,代码根据目标的位置和旋转值定位相机。transform.position行可能是此代码中最大的“啊哈!”,因为它提供了您以前从未见过的关键数学。将位置向量乘以四元数会导致位置根据该旋转而移动(请注意,旋转角度已使用 Quaternion.Euler 转换为四元数)。然后将此旋转的位置向量作为与角色位置的偏移量添加以计算相机的位置。图 8.7 说明了计算步骤,并提供了此概念相当密集的代码行的详细分解。
Next, the code positions the camera based on the position of the target and the rotation value. The transform.position line is probably the biggest “aha!” in this code, because it provides crucial math that you haven’t seen before. Multiplying a position vector by a quaternion results in a position that’s shifted over according to that rotation (note that the rotation angle was converted to a quaternion by using Quaternion.Euler). This rotated position vector is then added as the offset from the character’s position to calculate the position for the camera. Figure 8.7 illustrates the steps of the calculation and provides a detailed breakdown of this rather conceptually dense line of code.
Figure 8.7 The steps for calculating the camera’s position
注意:你们当中数学更敏锐的人可能会想,“嗯,第 2 章中提到的坐标系间变换...我在这里不能也这么做吗?”是的,你可以使用旋转坐标系来变换偏移位置以获得旋转偏移,但这需要首先设置旋转坐标系,而更直接的做法是不需要这个步骤。
NOTE The more mathematically astute among you may be thinking, “Hmm, that transforming-between-coordinate-systems thing in chapter 2 . . . can’t I do that here, too?” Yes, you could transform the offset position by using a rotated coordinate system to get the rotated offset, but that would require setting up the rotated coordinate system first, and it’s more straightforward not to need that step.
最后,代码使用LookAt()方法将相机指向目标;此函数将一个对象(不仅仅是相机)指向另一个对象。之前计算的旋转值用于将相机定位在目标周围的正确角度,但在此步骤中,相机仅被定位而不是旋转。因此,如果没有最后的LookAt()行,相机位置将围绕角色旋转,但不一定会看着它。继续注释掉该行以查看会发生什么。
Finally, the code uses the LookAt() method to point the camera at the target; this function points one object (not just cameras) at another object. The rotation value calculated previously was used to position the camera at the correct angle around the target, but in that step the camera was only positioned and not rotated. Thus, without the final LookAt() line, the camera position would orbit around the character but wouldn’t necessarily be looking at it. Go ahead and comment out that line to see what happens.
The camera has its script for orbiting around the player character; next up is code that moves the character around.
现在角色模型已导入 Unity,并且您已编写代码来控制相机视图,现在是时候编写用于在场景中移动的控件了。让我们编写与相机相关的控件,当按下箭头键时,该控件会将角色移动到各个方向,以及旋转角色以面向不同的方向。
Now that the character model is imported into Unity and you’ve written code to control the camera view, it’s time to program controls for moving around the scene. Let’s program camera-relative controls that’ll move the character in various directions when arrow keys are pressed, as well as rotate the character to face those different directions.
实现与相机相关的控制涉及两个主要步骤:首先旋转玩家角色以面向控制方向,然后向前移动角色。接下来让我们为这两个步骤编写代码。
Implementing camera-relative controls involves two primary steps: first rotate the player character to face the direction of the controls and then move the character forward. Let’s write the code for these two steps next.
第一的你将编写代码让角色面向箭头键的方向。创建一个名为RelativeMovement的 C# 脚本,使用清单 8.2 中的代码。将该脚本拖到玩家角色上,然后将相机链接到目标属性脚本组件(就像您将角色链接到相机脚本的目标一样)。现在,当您按下控件时,角色将面向不同的方向,面向相对于相机的方向,或者在您不按任何箭头键时(即使用鼠标旋转时)静止不动。
First you’ll write code to make the character face in the direction of the arrow keys. Create a C# script called RelativeMovement that uses the code from listing 8.2. Drag that script onto the player character and then link the camera to the target property of the script component (just as you linked the character to the target of the camera script). Now the character will face different directions when you press the controls, facing directions relative to the camera, or stand still when you’re not pressing any arrow keys (that is, when rotating using the mouse).
Listing 8.2 Rotating the character relative to the camera
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 RelativeMovement:MonoBehaviour {
[SerializeField] 变换目标; ❶
无效更新(){
Vector3 运动 = Vector3.zero; ❷
float horInput = Input.GetAxis("水平");
float vertInput = Input.GetAxis("垂直");
如果 (horInput != 0 || vertInput != 0) { ❸
Vector3 右 = 目标.右;
Vector3 前进 = Vector3.Cross(right, Vector3.up); ❹
运动 = (right * horInput) + (forward * vertInput); ❺
transform.旋转 = Quaternion.LookRotation(运动); ❻
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RelativeMovement : MonoBehaviour {
[SerializeField] Transform target; ❶
void Update() {
Vector3 movement = Vector3.zero; ❷
float horInput = Input.GetAxis("Horizontal");
float vertInput = Input.GetAxis("Vertical");
if (horInput != 0 || vertInput != 0) { ❸
Vector3 right = target.right;
Vector3 forward = Vector3.Cross(right, Vector3.up); ❹
movement = (right * horInput) + (forward * vertInput); ❺
transform.rotation = Quaternion.LookRotation(movement); ❻
}
}
}
❶ This script needs a reference to the object to move relative to.
❷ Start with vector (0, 0, 0) and add movement components progressively.
❶ Handle movement only while arrow keys are pressed.
❸ Calculate the player’s forward direction by using the cross product of the target’s right direction.
❹ Add together the input in each direction to get the combined movement vector.
❺ LookRotation() calculates a quaternion facing in that direction.
此清单的开头与清单 8.1 相同,都是使用序列化变量作为target。正如上一个脚本需要引用它将围绕的物体,此脚本需要引用它将相对于的物体移动。然后我们进入Update()函数。函数的第一行声明了一个Vector3值0 , 0 , 0。如果玩家按下任何按钮,剩余的代码将替换这个向量,但重要的是要有一个默认值,以防没有任何输入。
This listing starts the same way listing 8.1 did, with a serialized variable for target. Just as the previous script needed a reference to the object it would orbit around, this script needs a reference to the object it’ll move relative to. Then we get to the Update() function. The first line of the function declares a Vector3 value of 0, 0, 0. The remaining code will replace this vector if the player is pressing any buttons, but it’s important to have a default value in case there isn’t any input.
接下来,检查输入控件,就像您在之前的脚本中所做的一样。这里是移动向量中设置 X 和 Z 值的地方,用于在场景中水平移动。请记住,如果没有按下任何按钮,Input.GetAxis()将返回0 ,而当按下这些键时,它会在1和-1之间变化;将该值放入移动向量中会将移动设置为该轴的正向或负向(x 轴为左/右,z 轴为前/后)。
Next, check the input controls, just as you have in previous scripts. Here’s where X and Z values are set in the movement vector, for horizontal movement around the scene. Remember that Input.GetAxis() returns 0 if no button is pressed, and it varies between 1 and -1 when those keys are being pressed; putting that value in the movement vector sets the movement to the positive or negative direction of that axis (the x-axis is left/right, and the z-axis is forward/backward).
接下来的几行计算相机相对移动向量。具体来说,我们需要确定移动的侧向和向前方向。侧向方向很简单;目标变换具有一个名为right 的属性,它将指向相机的右侧,因为相机被设置为目标对象。向前方向比较棘手,因为相机向前向下倾斜到地面,但我们希望角色垂直于地面移动。可以使用叉积确定这个向前方向。
The next several lines calculate the camera-relative movement vector. Specifically, we need to determine the sideways and forward directions to move in. The sideways direction is easy; the target transform has a property called right, and that will point to the camera’s right because the camera was set as the target object. The forward direction is trickier, because the camera is angled forward and down into the ground, but we want the character to move around perpendicular to the ground. This forward direction can be determined using the cross product.
定义叉积是一种可以对两个向量执行的数学运算。长话短说,两个向量的叉积是一个指向两个输入向量的垂直方向的新向量。想想 3D 坐标轴:z 轴垂直于 x 轴和 y 轴。不要将叉积与点积混淆;点积(本章后面将解释)是一种不同但也很常见的向量数学运算。
DEFINITION The cross product is one kind of mathematical operation that can be done on two vectors. Long story short, the cross product of two vectors is a new vector pointed perpendicular to both input vectors. Think about the 3D coordinate axes: the z—axis is perpendicular to both the x- and y-axes. Don’t confuse cross product with dot product; the dot product (explained later in the chapter) is a different but also commonly seen vector math operation.
在这种情况下,两个输入向量是向右和向上方向。请记住,我们已经确定了相机的右侧。同时,Vector3具有几个常用方向的快捷属性,包括从地面直指上方的方向。垂直于这两个方向的向量指向相机所面对的方向,但垂直于地面对齐。
In this case, the two input vectors are the right and up directions. Remember that we already determined the camera’s right. Meanwhile, Vector3 has several shortcut properties for common directions, including the direction pointed straight up from the ground. The vector perpendicular to both of those points in the direction the camera faces, but aligned perpendicular to the ground.
添加每个方向的输入以获得组合的移动向量。最后一行代码通过使用Quaternion.LookRotation()将Vector3转换为四元数并分配该值,将该移动方向应用于角色。现在尝试运行游戏看看会发生什么!
Add the inputs in each direction to get the combined movement vector. The final line of code applies that movement direction to the character by converting Vector3 into a quaternion by using Quaternion.LookRotation() and assigning that value. Try running the game now to see what happens!
目前,角色在原地旋转而不移动;在下一节中,你将添加移动角色的代码大约。
Currently, the character is rotating in place without moving; in the next section, you’ll add code for moving the character around.
注意:由于横向移动与绕镜头旋转使用相同的键盘控制,因此角色将缓慢旋转,同时移动方向指向横向。此项目中需要双重控制。
NOTE Because moving sideways uses the same keyboard controls as orbiting the camera, the character will slowly rotate while the movement direction points sideways. This doubling up of the controls is desired behavior in this project.
作为您会回想起第 2 章,为了在场景中移动玩家,您需要向玩家对象添加一个角色控制器组件。选择玩家,然后选择组件 > 物理 > 角色控制器。在检查器中,您应该将控制器的半径稍微减小到0.4 ,但除此之外,默认设置对于此角色模型来说都是没问题的。以下是您需要在RelativeMovement脚本中添加的内容。
As you’ll recall from chapter 2, in order to move the player around the scene, you need to add a character controller component to the player object. Select the player and then choose Component > Physics > Character Controller. In the Inspector, you should slightly reduce the controller’s radius to 0.4, but otherwise the default settings are all fine for this character model. Here’s what you need to add in the RelativeMovement script.
Listing 8.3 Adding code to change the player’s position
使用System.Collections; 使用 System.Collections.Generic; 使用 UnityEngine; [RequireComponent(typeof(CharacterController))] ❶ 公共类 RelativeMovement:MonoBehaviour { ... 公共浮点移动速度=6.0f; 私人CharacterController charController; 无效开始(){ charController = GetComponent<CharacterController>(); ❷ } 无效更新(){ ... 运动 = (右 * 水平输入) + (前 * 垂直输入); 运动* = 移动速度; ❸ 运动 = Vector3.ClampMagnitude(运动,移动速度);❹ ... } 运动 *= Time.deltaTime; ❺ charController.移动(运动); } }
using System.Collections; using System.Collections.Generic; using UnityEngine; [RequireComponent(typeof(CharacterController))] ❶ public class RelativeMovement : MonoBehaviour { ... public float moveSpeed = 6.0f; private CharacterController charController; void Start() { charController = GetComponent<CharacterController>(); ❷ } void Update() { ... movement = (right * horInput) + (forward * vertInput); movement *= moveSpeed; ❸ movement = Vector3.ClampMagnitude(movement, moveSpeed); ❹ ... } movement *= Time.deltaTime; ❺ charController.Move(movement); } }
❶周围的行是放置 RequireComponent() 方法的上下文。
❶ The surrounding lines are context for placing the RequireComponent() method.
❷ A pattern you’ve seen in previous chapters, used for getting access to other components.
❸ The facing directions are magnitude 1, so multiply with the desired speed value.
❹ Limit diagonal movement to the same speed as movement along an axis.
❺ Always multiply movements by deltaTime to make them frame-rate independent.
如果您现在玩游戏,您会看到角色(保持 T 姿势)在场景中移动。几乎整个清单都是您已经看到的代码,因此我将简要回顾所有内容。
If you play the game now, you will see the character (stuck in a T-pose) moving around in the scene. Pretty much the entirety of this listing is code you’ve already seen, so I’ll review everything briefly.
首先,RequireComponent属性位于代码顶部。如第 2 章所述,RequireComponent将强制 Unity 确保 GameObject 具有传入命令的类型的组件。此行是可选的;您不必要求它,但如果没有此组件,脚本将出现错误。
First, a RequireComponent attribute is at the top of the code. As explained in chapter 2, RequireComponent will force Unity to make sure the GameObject has a component of the type passed into the command. This line is optional; you don’t have to require it, but without this component, the script will have errors.
接下来,声明一个移动值,然后获取此脚本对角色控制器的引用。正如您在前面章节中记得的那样,GetComponent()返回附加到给定对象的其他组件,如果要搜索的对象未明确定义,则假定它是this.gameObject.GetComponent()(与此脚本相同的对象)。
Next, a movement value is declared, followed by getting this script a reference to the character controller. As you’ll recall from previous chapters, GetComponent() returns other components attached to the given object, and if the object to search on isn’t explicitly defined, then it’s assumed to be this.gameObject.GetComponent() (the same object as this script).
移动值仍根据输入控件分配,但现在您还需要考虑移动速度。将所有移动轴乘以移动速度,然后使用Vector3.ClampMagnitude()将矢量的幅度限制为移动速度。需要限制,因为否则,对角线移动的幅度将大于直接沿轴移动的幅度(想象直角三角形的边和斜边)。
Movement values are still assigned based on the input controls, but now you also account for the movement speed. Multiply all movement axes by the movement speed, and then use Vector3.ClampMagnitude() to limit the vector’s magnitude to the movement speed. The clamp is needed because, otherwise, diagonal movement would have a greater magnitude than movement directly along an axis (picture the sides and hypotenuse of a right triangle).
最后,在最后,将移动值乘以deltaTime以获得与帧速率无关的移动(回想一下,与帧速率无关意味着角色在不同计算机上以相同的速度移动,并且帧速率不同)。将移动值传递给CharacterController.Move()以进行移动。
Finally, at the end, you multiply the movement values by deltaTime to get frame rate-independent movement (recall that frame rate-independent means the character moves at the same speed on different computers with different frame rates). Pass the movement values to CharacterController.Move() to make the movement.
This handles all the horizontal movement. Next, let’s take care of vertical movement.
在在上一节中,您编写了代码让角色在地面上奔跑。在章节介绍中,我还提到了让角色跳跃,现在让我们来做这件事。大多数第三人称游戏都有跳跃控制。即使没有,它们也几乎总是有角色从壁架上掉下来的垂直运动。我们的代码将处理跳跃和坠落。具体来说,这段代码会让重力始终将玩家拉下,但玩家跳跃时偶尔会施加向上的震动。
In the previous section, you wrote code to make the character run around on the ground. In the chapter introduction, I also mentioned making the character jump, so let’s do that now. Most third-person games do have a control for jumping. And even if they don’t, they almost always have vertical movement from the character falling off ledges. Our code will handle both jumping and falling. Specifically, this code will have gravity pulling the player down at all times, but occasionally an upward jolt will be applied when the player jumps.
在编写此代码之前,让我们在场景中添加一些凸起的平台。游戏目前没有可以跳跃或掉落的东西!创建更多立方体对象,然后修改它们的位置和比例,为玩家提供可以跳跃的平台。在示例项目中,我添加了两个立方体并使用了以下设置:位置5、0.75、5和比例4、1.5、4 ;位置1、1.5、5.5和比例4、3、4 。图8.8显示了凸起的平台。
Before you write this code, let’s add a few raised platforms to the scene. The game currently has nothing to jump on or fall from! Create a couple more cube objects, and then modify their positions and scale to give the player platforms to jump on. In the sample project, I added two cubes and used these settings: Position 5, 0.75, 5 and Scale 4, 1.5, 4; Position 1, 1.5, 5.5, and Scale 4, 3, 4. Figure 8.8 shows the raised platforms.
Figure 8.8 A couple of raised platforms added to the sparse scene
作为在您第一次开始编写清单 8.2 中的RelativeMovement脚本时提到过,移动值是分步计算的,并逐步添加到移动向量中。此清单将垂直移动添加到现有向量中。
As mentioned when you first started writing the RelativeMovement script in listing 8.2, the movement values are calculated in separate steps and added to the movement vector progressively. This listing adds vertical movement to the existing vector.
清单 8.4 在RelativeMovement脚本中添加垂直移动
Listing 8.4 Adding vertical movement to the RelativeMovement script
...
公共浮点跳跃速度 = 15.0f;
公共浮动重力= -9.8f;
公共浮动终端速度=-10.0f;
公共浮动minFall = -1.5f;
私有浮点垂直速度;
...
无效开始(){
vertSpeed = minFall; ❶
...
}
无效更新(){
...
如果 (charController.isGrounded) { ❷
如果 (Input.GetButtonDown("跳跃")) { ❸
垂直速度 = 跳跃速度;
} 别的 {
vertSpeed = 最小跌落;
}
}否则{ ❹
vertSpeed += 重力 * 5 * Time.deltaTime;
如果 (垂直速度 < 终端速度) {
垂直速度 = 终端速度;
}
}
运动.y = 垂直速度;
运动 *= Time.deltaTime; ❺
charController.移动(运动);
}
}...
public float jumpSpeed = 15.0f;
public float gravity = -9.8f;
public float terminalVelocity = -10.0f;
public float minFall = -1.5f;
private float vertSpeed;
...
void Start() {
vertSpeed = minFall; ❶
...
}
void Update() {
...
if (charController.isGrounded) { ❷
if (Input.GetButtonDown("Jump")) { ❸
vertSpeed = jumpSpeed;
} else {
vertSpeed = minFall;
}
} else { ❹
vertSpeed += gravity * 5 * Time.deltaTime;
if (vertSpeed < terminalVelocity) {
vertSpeed = terminalVelocity;
}
}
movement.y = vertSpeed;
movement *= Time.deltaTime; ❺
charController.Move(movement);
}
}
❶ Initialize the vertical speed to the minimum falling speed at the start of the existing function.
❷ CharacterController 有一个 isGrounded 属性,用于检查控制器是否在地面上。
❷ CharacterController has an isGrounded property to check if the controller is on the ground.
❸ React to the Jump button while on the ground.
❹ If not on the ground, apply gravity until terminal velocity is reached.
❺ This is existing code, simply for reference on where the new code goes.
像往常一样,你首先在脚本顶部添加一些新变量来表示各种移动值,并正确初始化这些值。然后,你跳到大if语句之后用于水平移动,其中您将添加另一个用于垂直移动的大if语句。具体来说,代码将检查角色是否在地面上,因为在每种情况下垂直速度的调整方式不同。CharacterController包含isGrounded用于检查角色是否在地面上;如果角色控制器的底部在上一帧与任何东西发生碰撞,则此值为true 。
As usual, you start by adding a few new variables to the top of the script for various movement values, and initialize the values correctly. Then, you skip down to just after the big if statement for horizontal movement, where you’ll add another big if statement for vertical movement. Specifically, the code will check whether the character is on the ground, because the vertical speed will be adjusted differently in each case. CharacterController includes isGrounded for checking whether the character is on the ground; this value is true if the bottom of the character controller collided with anything in the last frame.
如果角色在地面上,则垂直速度值(私有vertSpeed变量)应重置为零。角色在地面上时不会坠落,因此其垂直速度为 0;如果角色随后从壁架上走下来,您将获得一个漂亮、自然的动作,因为坠落速度将从零开始加速。
If the character is on the ground, the vertical speed value (the private vertSpeed variable) should be reset to nothing. The character isn’t falling while on the ground, so its vertical speed is 0; if the character then steps off a ledge, you’re going to get a nice, natural-looking motion because the falling speed will accelerate from nothing.
注意:垂直速度并非完全为0;您将值设置为minFall,即轻微向下移动,这样角色在水平奔跑时将始终压在地面上。在不平坦的地形上上下奔跑需要一些向下的力量。
NOTE Well, the vertical speed is not exactly 0; you’re setting the value to minFall, a slight downward movement, so that the character will always be pressing down against the ground while running around horizontally. Some downward force is required for running up and down on uneven terrain.
如果单击跳跃按钮,则会出现此地面速度值的异常。在这种情况下,垂直速度应设置为较高的数字。if语句检查GetButtonDown(),这是一种新的输入函数,其工作原理与GetAxis()非常相似,可返回指示的输入控件的状态。与水平和垂直输入轴非常相似,分配给 Jump 的确切键是通过转到“编辑”>“项目设置”下的“输入管理器”设置来定义的(默认键分配为 Space,即空格键)。
The exception to this grounded speed value occurs if the jump button is clicked. In that case, the vertical speed should be set to a high number. The if statement checks GetButtonDown(), a new input function that works much like GetAxis() does, returning the state of the indicated input control. And much like Horizontal and Vertical input axes, the exact key assigned to Jump is defined by going to Input Manager settings under Edit > Project Settings (the default key assignment is Space—that is, the spacebar).
回到更大的if条件,如果角色不在地面上,则垂直速度应因重力而不断降低。请注意,此代码不是简单地设置速度值,而是将其减小;这样,它就不是恒定速度,而是向下加速度,从而产生逼真的下落运动。跳跃将以自然的弧线发生,因为角色的向上速度逐渐减小到 0,并开始下落。
Getting back to the larger if condition, if the character is not on the ground, then the vertical speed should be constantly reduced by gravity. Note that this code doesn’t simply set the speed value but rather decrements it; this way, it’s not a constant speed but rather a downward acceleration, resulting in a realistic falling movement. Jumping will happen in a natural arc, as the character’s upward speed gradually reduces to 0 and it starts falling instead.
最后,代码确保向下速度不超过终端速度。请注意,运算符小于而不是大于,因为向下是负速度值。然后,在大if语句之后,将计算出的垂直速度分配给运动矢量的 y 轴。
Finally, the code makes sure the downward speed doesn’t exceed terminal velocity. Note that the operator is less than and not greater than, because downward is a negative speed value. Then, after the big if statement, assign the calculated vertical speed to the y-axis of the movement vector.
这就是逼真的垂直运动所需要的一切!通过在角色不在地面时施加恒定的向下加速度,并在角色在地面时适当调整速度,代码可以创建良好的坠落行为。但这一切都取决于正确检测地面,并且仍然存在一个微妙的故障,您需要使固定。
And that’s all you need for realistic vertical movement! By applying a constant downward acceleration when the character isn’t on the ground, and adjusting the speed appropriately when the character is on the ground, the code creates nice falling behavior. But this all depends on detecting the ground correctly, and a subtle glitch remains that you need to fix.
作为在上一节中解释过,isGrounded属性CharacterController的表示角色控制器的底部在上一帧中是否与任何东西发生碰撞。虽然这种检测地面的方法在大多数情况下都有效,但您可能会注意到角色在离开边缘时似乎漂浮在空中。
As explained in the previous section, the isGrounded property of CharacterController indicates whether the bottom of the character controller collided with anything in the last frame. Although this approach to detecting the ground works the majority of the time, you’ll probably notice that the character seems to float in the air while stepping off edges.
这是因为角色的碰撞区域是一个环绕的胶囊(选择角色对象时可以看到它),并且当玩家从平台边缘走下来时,这个胶囊的底部仍然会与地面接触。图 8.9 说明了这个问题。这根本行不通!
That’s because the collision area of the character is a surrounding capsule (you can see it when you select the character object), and the bottom of this capsule will still be in contact with the ground when the player steps off the edge of the platform. Figure 8.9 illustrates the problem. This won’t do at all!
Figure 8.9 Diagram showing the character controller capsule touching the platform edge
同样,如果角色站在斜坡上,当前的地面检测将导致有问题的行为。现在尝试通过在凸起的平台上创建一个倾斜的块。创建一个新的立方体对象并将其变换值设置为位置-1.5、1.5、5 、旋转0、0、-25和比例1、4、4。
Similarly, if the character stands on a slope, the current ground detection will cause problematic behavior. Try it now by creating a sloped block against the raised platforms. Create a new cube object and set its transform values to Position -1.5, 1.5, 5, Rotation 0, 0, -25, and Scale 1, 4, 4.
如果你从地面跳上斜坡,你会发现你可以从斜坡中途再次跳跃,从而爬到顶部。这是因为斜坡斜着接触胶囊底部,而代码目前认为底部的任何碰撞都是坚实的基础。同样,这不行;角色应该滑回原位,而不是有一个坚实的基础来跳跃。
If you jump onto the slope from the ground, you’ll find that you can jump again from midway up the slope and thereby ascend to the top. That’s because the slope touches the bottom of the capsule obliquely, and the code currently considers any collision on the bottom to be solid footing. Again, this won’t do; the character should slide back down, not have a solid footing to jump from.
注意:只有在陡坡上才需要滑回原位。在平缓的斜坡上,例如不平坦的地面,您希望玩家不受任何影响地四处奔跑。如果您想要一个进行测试,请创建一个立方体,将其设置为位置5.25、0.25、0.25,旋转0、90、75,比例1、6、3 ,制作一个平缓的斜坡。
NOTE Sliding back down is desired only on steep slopes. On shallow slopes, such as uneven ground, you want the player to run around unaffected. If you want one to test on, make a shallow ramp by creating a cube and set it to Position 5.25, 0.25, 0.25, Rotation 0, 90, 75, Scale 1, 6, 3.
所有这些问题都有相同的根本原因:检查角色底部的碰撞并不是确定角色是否在地面上的好方法。相反,让我们使用射线投射来检测地面。在第 3 章中,AI 使用射线投射来检测前方的障碍物;让我们使用相同的方法来检测角色下方的表面。从玩家的位置垂直向下投射射线。如果它在角色脚下下方击中,则玩家站在地面上。
All these problems have the same root cause: checking for collisions on the bottom of the character isn’t a great way to determine whether the character is on the ground. Instead, let’s use raycasting to detect the ground. In chapter 3, the AI used raycasting to detect obstacles in front of it; let’s use the same approach to detect surfaces below the character. Cast a ray straight down from the player’s position. If it registers a hit just below the character’s feet, the player is standing on the ground.
这引入了一种需要处理的新情况:当射线投射未检测到角色下方的地面,但角色控制器与地面发生碰撞时。如图 8.9 所示,当角色走出边缘时,胶囊仍与平台发生碰撞。图 8.10 将射线投射添加到图表中以显示现在会发生什么:射线没有击中平台,但胶囊确实接触了边缘。代码需要处理这种特殊情况。
This introduces a new situation to handle: when the raycast doesn’t detect ground below the character, but the character controller is colliding with the ground. As in figure 8.9, the capsule still collides with the platform while the character is walking off the edge. Figure 8.10 adds raycasting to the diagram to show what will happen now: the ray doesn’t hit the platform, but the capsule does touch the edge. The code needs to handle this special situation.
Figure 8.10 Diagram of raycasting downward while stepping off a ledge
在这种情况下,代码应该使角色从壁架上滑落。角色仍会掉落(因为它没有站在地面上),但它也会从碰撞点推开(因为它需要将胶囊移离它撞击的平台)。因此,代码将使用角色控制器检测碰撞并通过轻推来响应这些碰撞。此列表使用我们刚刚讨论的所有内容调整垂直运动。
In this case, the code should make the character slide off the ledge. The character will still fall (because it’s not standing on the ground), but it’ll also push away from the point of collision (because it needs to move the capsule away from the platform it’s hitting). Thus, the code will detect collisions with the character controller and respond to those collisions by nudging away. This listing adjusts the vertical movement with everything we just discussed.
Listing 8.5 Using raycasting to detect the ground
... 私有 ControllerColliderHit 接触; ❶ ... bool hitGround = false; RaycastHit 命中; 如果 (vertSpeed < 0 && ❷ Physics.Raycast(transform.position, Vector3.down, out hit)) { 浮动检查 = ❸ (charController.高度+charController.半径)/1.9f; hitGround = hit.距离<=检查; } 如果 (hitGround) { ❹ 如果(Input.GetButtonDown("跳转")){ 垂直速度 = 跳跃速度; } 别的 { vertSpeed = 最小跌落; } } 别的 { vertSpeed += 重力 * 5 * Time.deltaTime; 如果 (垂直速度 < 终端速度) { 垂直速度 = 终端速度; } 如果 (charController.isGrounded) { ❺ 如果 (Vector3.Dot(movement, contact.normal) < 0) { ❻ 运动 = 接触.正常 * 移动速度; } 别的 { 运动+=接触.正常*移动速度; } } } 运动.y = 垂直速度; 运动 *=时间.deltaTime; charController.移动(运动); } void OnControllerColliderHit(ControllerColliderHit hit) { ❼ 接触 = 命中; } }
... private ControllerColliderHit contact; ❶ ... bool hitGround = false; RaycastHit hit; if (vertSpeed < 0 && ❷ Physics.Raycast(transform.position, Vector3.down, out hit)) { float check = ❸ (charController.height + charController.radius) / 1.9f; hitGround = hit.distance <= check; } if (hitGround) { ❹ if (Input.GetButtonDown("Jump")) { vertSpeed = jumpSpeed; } else { vertSpeed = minFall; } } else { vertSpeed += gravity * 5 * Time.deltaTime; if (vertSpeed < terminalVelocity) { vertSpeed = terminalVelocity; } if (charController.isGrounded) { ❺ if (Vector3.Dot(movement, contact.normal) < 0) { ❻ movement = contact.normal * moveSpeed; } else { movement += contact.normal * moveSpeed; } } } movement.y = vertSpeed; movement *= Time.deltaTime; charController.Move(movement); } void OnControllerColliderHit(ControllerColliderHit hit) { ❼ contact = hit; } }
❶ Needed to store collision data between functions
❷ Check if the player is falling.
❸ Distance to check against (extend slightly beyond the bottom of the capsule)
❹ Instead of using isGrounded, check the raycasting result.
❺ Raycasting didn’t detect ground, but the capsule is touching the ground.
❻ Respond slightly differently depending on whether the character is facing the contact point.
❼ Store the collision data in the callback when a collision is detected.
此清单包含与上一个清单相同的许多代码;新代码散布在现有移动脚本中,并且此清单需要现有代码作为上下文。第一行在RelativeMovement脚本的顶部添加了一个新变量。此变量用于存储有关函数之间碰撞的数据。
This listing contains much of the same code as the previous listing; the new code is interspersed throughout the existing movement script, and this listing needs the existing code for context. The first line adds a new variable to the top of the RelativeMovement script. This variable is used to store data about collisions between functions.
接下来的几行代码用于射线投射。此代码也位于水平移动下方,但在用于垂直移动的if语句之前。实际的Physics.Raycast()调用应该与前面的章节相似,但这次的具体参数不同。虽然投射射线的位置相同(角色的位置),但这次方向是向下而不是向前。然后,检查射线投射击中某物时的距离;如果击中的距离是角色脚的距离,则角色站在地面上,因此将hitGround设置为true。
The next several lines do raycasting. This code also goes below horizontal movement but before the if statement for vertical movement. The actual Physics.Raycast() call should be familiar from previous chapters, but the specific parameters are different this time. Although the position to cast a ray from is the same (the character’s position), the direction will be down this time instead of forward. Then, you check how far away the raycast was when it hit something; if the distance of the hit is at the distance of the character’s feet, the character is standing on the ground, so set hitGround to true.
警告:检查距离的计算方式并不明显,因此让我们详细了解一下。首先,取角色控制器的高度(即不包含圆角末端的高度),然后添加圆角末端。将此值除以二,因为射线是从角色中间(即已经到达一半)投射的,从而得到到角色底部的距离。但您确实希望检查角色底部以外的距离,以解决射线投射中的微小误差,因此除以 1.9 而不是 2 会得到略微过远的距离。
WARNING The way the check distance is calculated is not obvious, so let’s go over that in detail. First, take the height of the character controller (which is the height without the rounded ends) and then add the rounded ends. Divide this value in half because the ray was cast from the middle of the character (that is, already halfway down) to get the distance to the bottom of the character. But you really want to check a little beyond the bottom of the character to account for tiny inaccuracies in the raycasting, so divide by 1.9 instead of 2 to get a distance that’s slightly too far.
完成此射线投射后,在if语句中使用hitGround而不是isGrounded进行垂直移动。大部分垂直移动代码将保持不变,但添加代码来处理角色控制器与地面碰撞的情况,即使玩家不在地面上(即,当玩家走出平台边缘时)。我们添加了一个新的isGrounded条件,但请注意,它嵌套在hitGround条件内,因此仅当hitGround未检测到地面时才会检查isGrounded 。
Having done this raycasting, use hitGround instead of isGrounded in the if statement for vertical movement. Most of the vertical movement code will remain the same, but add code to handle when the character controller collides with the ground even though the player isn’t over the ground (that is, when the player walks off the edge of the platform). We’ve added a new isGrounded conditional, but note that it’s nested inside the hitGround conditional so that isGrounded is checked only when hitGround doesn’t detect the ground.
碰撞数据包括正常属性(再次强调,法线向量表示某物面向哪个方向)它告诉我们远离碰撞点的方向。但有一点很棘手,那就是您希望根据玩家已经移动的方向以不同的方式处理远离接触点的轻推。当之前的水平移动朝向平台时,您需要替换该移动,以便角色不会继续朝错误的方向移动;但是当背对边缘时,您需要添加之前的水平移动,以保持向前的动量远离边缘。可以使用点积确定相对于碰撞点的移动向量朝向。
The collision data includes a normal property (again, a normal vector says which way something is facing) that tells us the direction to move away from the point of collision. But one tricky thing is that you want the nudge away from the contact point to be handled differently depending on in which direction the player is already moving. When the previous horizontal movement is toward the platform, you want to replace that movement so that the character won’t keep moving in the wrong direction; but when facing away from the edge, you want to add to the previous horizontal movement in order to keep the forward momentum away from the edge. The movement vector’s facing relative to the point of collision can be determined using the dot product.
定义点积是另一个可以对两个向量执行的数学运算。两个向量的点积介于N和-N之间(N由输入向量的幅度相乘确定)。正N表示它们指向完全相同的方向,而-N表示它们指向完全相同的方向。不要混淆点积和叉积;叉积是一种不同但也很常见的向量数学运算。
DEFINITION The dot product is another mathematical operation that can be done on two vectors. The dot product of two vectors ranges between N and -N (with N determined by multiplying the magnitude of the input vectors). Positive N means they point in exactly the same direction, and -N means they point in exactly opposite directions. Don’t confuse dot product and cross product; the cross product is a different but also commonly seen vector math operation.
Vector3包含一个Dot()函数,用于计算两个给定向量的点积。如果计算移动向量和碰撞法线之间的点积,当两个方向彼此背离时,将返回负数;当移动和碰撞朝向同一方向时,将返回正数。
Vector3 includes a Dot() function to calculate the dot product of two given vectors. If you calculate the dot product between the movement vector and the collision normal, that will return a negative number when the two directions face away from each other, and a positive number when the movement and the collision face the same direction.
清单 8.5 的最后为脚本添加了一个新方法。在前面的代码中,你检查了碰撞法线,但这些信息从何而来?事实证明,与角色控制器的碰撞是通过名为OnControllerColliderHit 的回调函数报告的();为了响应脚本中其他任何地方的碰撞数据,必须将数据存储在外部变量中。这就是该方法在这里所做的全部工作:将碰撞数据存储在接触中,以便可以在Update()方法中使用该数据。
The very end of listing 8.5 adds a new method to the script. In the previous code, you were checking the collision normal, but where did that information come from? It turns out that collisions with the character controller are reported through a callback function called OnControllerColliderHit() that MonoBehaviour provides; in order to respond to the collision data anywhere else in the script, that data must be stored in an external variable. That’s all the method is doing here: storing the collision data in contact so that this data can be used within the Update() method.
现在,平台边缘和斜坡上的错误已得到纠正。继续玩,通过跨过边缘并跳上陡坡来测试它。这个移动演示几乎完成了。角色在场景中移动正确,所以只剩下一件事:让角色从这T 型姿势。
Now the errors are corrected around platform edges and on slopes. Go ahead and play to test it out by stepping over edges and jumping onto the steep slope. This movement demo is almost complete. The character is moving around the scene correctly, so only one thing remains: animating the character out of the T-pose.
除了网格几何体定义的形状越复杂,人形角色就越需要动画。在第 4 章中,您了解到动画是定义相关 3D 对象运动的信息包。我给出的具体示例是角色四处走动,而这种情况正是您现在要做的!
Besides the more complex shape defined by mesh geometry, a humanoid character needs animations. In chapter 4, you learned that an animation is a packet of information that defines movement of the associated 3D object. The concrete example I gave was of a character walking around, and that situation is exactly what you’re going to be doing now!
角色将在场景中奔跑,因此您将分配动画,使其手臂和腿来回摆动。图 8.11 显示了当角色在场景中移动时播放动画时游戏的外观。
The character is going to run around the scene, so you’ll assign animations that make the arms and legs swing back and forth. Figure 8.11 shows what the game will look like when the character has an animation playing while it moves around the scene.
Figure 8.11 Character moving around with a run animation playing
理解 3D 动画的一个好比喻是木偶戏:3D 模型是木偶,动画师是木偶戏演员,动画是木偶动作的记录。动画可以通过几种方法创建;现代游戏中的大多数角色动画(当然是本章角色的所有动画)都使用一种称为骨骼动画的技术。
A good analogy for understanding 3D animation is puppeteering: 3D models are the puppets, the animator is the puppeteer, and an animation is a recording of the puppet’s movements. Animations can be created with a few approaches; most character animation in modern games (certainly all the animations on this chapter’s character) uses a technique called skeletal animation.
定义在骨骼动画中,在模型内部设置一系列骨骼,然后在动画过程中移动骨骼。当骨骼移动时,与该骨骼关联的模型表面也会随之移动。
DEFINITION In skeletal animation, a series of bones is set up inside the model, and then the bones are moved around during the animation. When a bone moves, the model’s surface linked to that bone moves along with it.
顾名思义,骨骼动画在模拟角色内部的骨骼时最直观(图 8.12 说明了这一点),但骨骼是一种抽象概念,每当您希望模型弯曲和伸缩,同时仍具有明确的运动结构时(例如,挥动的触手),它就很有用。虽然骨骼刚性移动,但骨骼周围的模型表面可以弯曲和伸缩。
As the name implies, skeletal animation makes the most intuitive sense when simulating the skeleton inside a character (figure 8.12 illustrates this), but the skeleton is an abstraction that’s useful anytime you want a model to bend and flex while still having a definite structure to its movement (for example, a tentacle that waves around). Although the bones move rigidly, the model surface around the bones can bend and flex.
Figure 8.12 Skeletal animation of a humanoid character
要实现图 8.11 所示的结果,需要几个步骤:首先,在导入的文件中定义动画剪辑,然后设置控制器来播放这些动画剪辑,最后将该动画控制器合并到代码中。角色模型上的动画将根据您编写的运动脚本播放。
Achieving the result illustrated in figure 8.11 involves several steps: first, define animation clips in the imported file, then set up the controller to play those animation clips, and finally, incorporate that animation controller in your code. The animations on the character model will be played back according to the movement scripts you’ll write.
当然,在执行任何这些步骤之前,您需要做的第一件事就是打开动画系统。在项目视图中选择玩家模型,以在检查器中查看其导入设置。选择动画选项卡并确保选中导入动画。然后转到 Rig 选项卡并将动画类型从通用切换到人形(这自然是一个人形角色)。请注意,最后一个菜单还有一个旧版设置;通用和人形都是 Mecanim 的总称中的设置。
Of course, the very first thing you need to do, before any of those steps, is turn on the animation system. Select the player model in the Project view to see its Import settings in the Inspector. Select the Animation tab and make sure Import Animation is checked. Then go to the Rig tab and switch Animation Type from Generic to Humanoid (this is a humanoid character, naturally). Note that this last menu also has a Legacy setting; Generic and Humanoid are both settings within the umbrella term Mecanim.
单击检查器底部的“应用”按钮将这些设置锁定到导入的模型上,然后继续定义动画剪辑。
Click the Apply button at the bottom of the Inspector to lock these settings onto the imported model and then continue defining animation clips.
警告您可能会注意到控制台中出现一条警告(不是错误),内容为:转换警告:spine3 介于人形变换之间。该特定警告无需担心;它表示导入模型中的骨架除了 Mecanim 预期的骨架之外,还具有额外的骨骼。
WARNING You may notice a warning (not an error) in the console that says, conversion warning: spine3 is between humanoid transforms. That specific warning isn’t a cause for worry; it indicates that the skeleton in the imported model has extra bones beyond the skeleton that Mecanim expects.
这为角色设置动画的第一步是定义将要播放的各种动画剪辑。如果你想象一个栩栩如生的角色,不同的动作可以在不同的时间发生:有时玩家四处奔跑,有时玩家在平台上跳跃,有时角色只是站在那里,双臂垂下。每个动作都是一个可以单独播放的单独剪辑。
The first step in setting up animations for our character is defining the various animation clips that’ll be played. If you think about a lifelike character, different movements can happen at different times: sometimes the player is running around, sometimes the player is jumping on platforms, and sometimes the character is just standing there with its arms down. Each movement is a separate clip that can play individually.
通常,导入的动画是一条较长的时间轴,可以将其分割成较短的单个动画。要分割动画剪辑,首先选择检查器中的“动画”选项卡。您将看到一个剪辑面板,如图 8.13 所示;它列出了所有定义的动画剪辑,这些剪辑最初是一个导入的剪辑。您会注意到列表底部有 + 和 - 按钮;您可以使用这些按钮添加和删除列表中的剪辑。最终,您需要为这个角色制作四个剪辑,因此在工作时根据需要添加和删除剪辑。
Often, imported animations come as a single long timeline that can be cut up into shorter individual animations. To split up the animation clips, first select the Animations tab in the Inspector. You’ll see a Clips panel, shown in figure 8.13; this lists all the defined animation clips, which initially are one imported clip. You’ll notice + and - buttons at the bottom of the list; you use these buttons to add and remove clips on the list. Ultimately, you need four clips for this character, so add and remove clips as necessary while you work.
Figure 8.13 The Clips list in Animation settings
当您选择一个剪辑时,有关该剪辑的信息(如图 8.14 所示)将显示在列表下方的区域中。此信息区域的顶部显示此剪辑的名称,您可以输入新名称。将第一个剪辑命名为idle。定义此动画剪辑的开始和结束帧;这允许您从较长的导入动画中切出一个块。idle 动画从总时间线的第 3 帧到第 141 帧,因此请输入开始和结束的数字。接下来是循环设置。
When you select a clip, information about that clip (shown in figure 8.14) will appear in the area below the list. The top of this information area shows the name of this clip, and you can type in a new name. Name the first clip idle. Define Start and End frames for this animation clip; this allows you to slice a chunk out of the longer imported animation. The idle animation goes from frames 3 to 141 of the total timeline, so enter those numbers for Start and End. Next up are the Loop settings.
定义 循环指反复播放的录制内容。循环动画剪辑是指播放到结尾后立即从头开始播放的动画剪辑。
DEFINITION Loop refers to a recording that plays over and over repeatedly. A looping animation clip is one that plays again from the start as soon as playback reaches the end.
Figure 8.14 Information about the selected animation clip
空闲动画会循环播放,因此请选择“循环时间”和“循环姿势”。顺便说一句,绿色指示点会告诉您剪辑开头的姿势是否与结尾的姿势相匹配,以实现正确的循环;当姿势有些偏离时,此指示器会变成黄色,而当开始和结束姿势完全不同时,指示器会变成红色。
The idle animation loops, so select both Loop Time and Loop Pose. Incidentally, the green indicator dot tells you when the pose at the beginning of the clip matches the pose at the end for correct looping; this indicator turns yellow when the poses are somewhat off, and it turns red when the start and end poses are completely different.
在循环设置下方是一系列与根变换相关的设置。根这个词对于骨骼动画和 Unity 中连接的层次结构的含义相同:根对象是其他所有对象都连接到的基础对象。因此,动画根可以被认为是角色的基础,其他所有对象都相对于该基础移动。
Below the Loop settings is a series of settings related to the root transform. The word root means the same thing for skeletal animation as it does for a hierarchy connected within Unity: the root object is the base object that everything else is connected to. Thus, the animation root can be thought of as the base of the character, and everything else moves relative to that base.
可以使用一些设置来设置该基础,在处理自己的动画时,您可能想在这里进行实验。但就我们的目的而言,三个“基于”菜单应按顺序设置为“身体方向”、“重心”和“重心”。
A few settings can be used for setting up that base, and you may want to experiment here when working with your own animations. For our purposes, though, the three Based Upon menus should be set to Body Orientation, Center Of Mass, and Center Of Mass, in that order.
现在单击“应用”,您就为角色添加了一个空闲动画剪辑。对另外两个剪辑执行相同操作:行走从第 144 帧开始,到第 169 帧结束,奔跑从第 171 帧开始,到第 190 帧结束。所有其他设置应与空闲设置相同,因为它们也是动画循环。
Now click Apply and you’ve added an idle animation clip to your character. Do the same for two more clips: walk starts at frame 144 and ends at 169, and run starts at 171 and ends at 190. All the other settings should be the same as for idle because they’re also animation loops.
第四个动画剪辑是跳跃,该剪辑的设置略有不同。首先,这不是循环,而是静止姿势,因此不要选择循环时间。将开始和结束设置为 190.5 和 191;这是一个单帧姿势,但 Unity 要求开始和结束不同。由于这些棘手的数字,下面的动画预览看起来不太正确,但这个姿势在游戏中看起来不错。单击应用以确认新的动画剪辑,然后继续下一步:创建动画器控制器。
The fourth animation clip is jump, and the settings for that clip differ a bit. First, this isn’t a loop but rather a still pose, so don’t select Loop Time. Set the Start and End to 190.5 and 191; this is a single-frame pose, but Unity requires that Start and End be different. The animation preview below won’t look quite right because of these tricky numbers, but this pose will look fine in the game. Click Apply to confirm the new animation clips, and then move on to the next step: creating the animator controller.
这下一步是为该角色创建动画控制器。此步骤允许我们设置动画状态并在这些状态之间创建过渡。在不同的动画状态下播放各种动画剪辑,然后我们的脚本将导致控制器在动画状态之间切换。
The next step is to create the animator controller for this character. This step allows us to set up animation states and create transitions between those states. Various animation clips are played during different animation states, and then our scripts will cause the controller to shift between animation states.
这似乎是一种奇怪的间接方式——将控制器的抽象放在我们的代码和实际的动画播放之间。您可能熟悉允许您直接从代码播放动画的系统;事实上,旧的 Legacy 动画系统正是以这种方式工作的,使用像Play("idle")这样的调用。但这种间接方式使我们能够在模型之间共享动画,而不是只能播放此模型内部的动画。在本章中,我们不会利用此功能,但请记住,当您处理较大的项目时,它会很有帮助。您可以从多个来源(包括多个动画师)获取动画,也可以从在线商店(例如 Unity Asset Store)购买单个动画。
This might seem like an odd bit of indirection—putting the abstraction of a controller between our code and the actual playing of animations. You may be familiar with systems that enable you to play animations directly from your code; indeed, the old Legacy animation system worked in exactly that way, with calls like Play("idle"). But this indirection enables us to share animations between models, rather than being able to play only animations that are internal to this model. In this chapter, we won’t take advantage of this ability, but keep in mind that it can be helpful when you’re working on a larger project. You can obtain your animations from several sources, including multiple animators, or you can buy individual animations from stores online (such as the Unity Asset Store).
首先创建一个新的动画控制器资源(Assets > Create > Animator Controller — 不是 Animation,这是一种不同的资源)。在 Project 视图中,你会看到一个图标,上面有一个看起来很有趣的线条网络(见图 8.15);将此资源重命名为player。选择场景中的角色,你会注意到这个对象有一个名为 Animator 的组件;除了 Transform 组件和你添加的其他组件之外,任何可以动画的模型都有这个组件。Animator 组件有一个 Controller 插槽,你可以用它来链接特定的动画控制器,因此请将你的新控制器资源拖放进去(并确保取消选中 Apply Root Motion)。
Begin by creating a new animator controller asset (Assets > Create > Animator Controller—not Animation, a different sort of asset). In the Project view, you’ll see an icon with a funny-looking network of lines on it (see figure 8.15); rename this asset player. Select the character in the scene and you’ll notice this object has a component called Animator; any model that can be animated has this component, in addition to the Transform component and whatever else you’ve added. The Animator component has a Controller slot for you to link a specific animator controller, so drag and drop your new controller asset (and be sure to uncheck Apply Root Motion).
Figure 8.15 Animator controller and Animator component
动画控制器是一棵连接节点的树(因此该资产上有图标),您可以通过打开动画视图来查看和操作它。这是另一个视图,就像场景或项目(如图 8.16 所示),只是默认情况下不打开此视图。选择窗口 > 动画,然后从此菜单中选择动画器(注意不要与动画窗口混淆;这是与动画器不同的选择)。此处显示的节点网络是当前选择的动画控制器(或所选角色上的动画控制器)。
The animator controller is a tree of connected nodes (hence the icon on that asset) that you can see and manipulate by opening the Animator view. This is another view, just like Scene or Project (shown in figure 8.16), except this view isn’t open by default. Choose Window > Animation and select Animator from this menu (be careful not to get confused with the Animation window; that’s a separate selection from Animator). The node network displayed here is whichever animator controller is currently selected (or the animator controller on the selected character).
Figure 8.16 The Animator view with our completed animator controller
提示请记住,您可以在 Unity 中移动选项卡,并将它们停靠在您想要组织界面的任何位置。我喜欢将 Animator 停靠在“场景”和“游戏”选项卡旁边。
TIP Remember that you can move tabs around in Unity and dock them wherever you like to organize the interface. I like to dock the Animator right next to the Scene and Game tabs.
最初,我们只有两个默认节点,即 Entry 和 Any State。您不会使用 Any State 节点。相反,您将拖入动画剪辑来创建新节点。在 Project 视图中,单击模型资产侧面的箭头以展开该资产并查看其包含的内容。此资产的内容包括您定义的动画剪辑(参见图 8.17),因此将这些剪辑拖入 Animator 视图。不要理会行走动画(这可能对其他项目有用),只需拖入空闲、奔跑和跳跃即可。
Initially, we have only two default nodes, for Entry and Any State. You’re not going to use the Any State node. Instead, you’ll drag in animation clips to create new nodes. In the Project view, click the arrow on the side of the model asset to expand that asset and see what it contains. Among the contents of this asset are the animation clips you defined (see figure 8.17), so drag those clips into the Animator view. Don’t bother with the walking animation (that could be useful for other projects) and drag in idle, run, and jump.
Figure 8.17 Expanded model asset in Project view
右键单击“空闲”节点并选择“设置为图层默认状态”。该节点将变为橙色,而其他节点保持灰色;默认动画状态是游戏进行任何更改之前节点网络的起始状态。您需要使用表示动画状态之间转换的线条将节点连接在一起;右键单击一个节点并选择“进行转换”以开始拖出一个箭头,您可以单击该箭头连接另一个节点。按照图 8.16 所示的模式连接节点(确保大多数节点在两个方向上都进行转换,但不是从跳跃到运行)。这些转换线决定了动画状态如何相互连接,并控制游戏过程中从一个状态到另一个状态的变化。
Right-click the Idle node and select Set As Layer Default State. That node will turn orange while the other nodes stay gray; the default animation state is where the network of nodes starts before the game has made any changes. You’ll need to link the nodes together with lines indicating transitions between animation states; right-click a node and select Make Transition to start dragging out an arrow that you can click on another node to connect. Connect nodes in the pattern shown in figure 8.16 (be sure to make transitions in both directions for most nodes, but not from jump to run). These transition lines determine how the animation states connect to each other and control the changes from one state to another during the game.
过渡依赖于一组控制值,因此让我们创建这些参数。左上角是“参数”选项卡(如图 8.16 所示);单击该选项卡可查看带有 + 按钮的面板,用于添加参数。添加一个名为Speed的浮点数和一个名为Jumping的布尔值。这些值将由我们的代码调整,它们将触发动画状态之间的过渡。单击过渡线可在检查器中查看它们的设置(参见图 8.18)。
The transitions rely on a set of controlling values, so let’s create those parameters. At the top left is the Parameters tab (shown previously in figure 8.16); click that to see a panel with a + button for adding parameters. Add a float called Speed and a Boolean called Jumping. Those values will be adjusted by our code, and they’ll trigger transitions between animation states. Click the transition lines to see their settings in the Inspector (see figure 8.18).
Figure 8.18 Transition settings in the Inspector
在这里,您可以调整动画状态在参数改变时的变化方式。例如,单击“空闲到运行”过渡以调整该过渡的条件。在“条件下”,添加一个并将其设置为“速度”、“更大”和“0.1”。关闭“有退出时间”(这将强制播放整个动画,而不是在过渡发生时立即缩短)。然后,单击“设置”标签旁边的箭头以查看整个菜单;其他过渡应该能够中断此过渡,因此将“中断源”菜单从“无”更改为“当前状态”。对表 8.1 中的所有过渡重复此操作。
Here’s where you’ll adjust how the animation states change when the parameters change. For example, click the Idle-to-Run transition to adjust the conditions of that transition. Under Conditions, add one and set it to Speed, Greater, and 0.1. Turn off Has Exit Time (that would force playing the animation all the way through, as opposed to cutting short immediately when the transition happens). Then, click the arrow next to the Settings label to see that entire menu; other transitions should be able to interrupt this one, so change the Interruption Source menu from None to Current State. Repeat this for all the transitions in table 8.1.
Table 8.1 Conditions for all transitions in this animator controller
除了这些基于菜单的设置之外,还有一个复杂的可视化界面,如图 8.18 所示,位于条件设置上方。此图表允许您直观地调整过渡的时间长度。对于 Idle 和 Run 之间的过渡,默认过渡时间看起来不错,但所有往返于 Jump 的过渡都应该更短,以便角色能够更快地捕捉到跳跃动画。图表的阴影区域表示过渡需要多长时间;要查看更多详细信息,请按住 Alt 键 + 左键单击(或在 Mac 上按住 Option 键 + 左键单击)图表以平移它,然后按住 Alt 键 + 右键单击以缩放它(这些控件与在场景视图中导航相同)。使用阴影区域顶部的箭头将其缩小到所有三个 Jump 过渡的 4 毫秒以下。
In addition to these menu-based settings is a complex visual interface, shown in figure 8.18, just above the Condition setting. This graph allows you to visually adjust the length in time of a transition. The default transition time looks fine for both transitions between Idle and Run, but all of the transitions to and from Jump should be shorter so that the character will snap faster to the jump animation. The shaded area of the graph indicates how long the transition takes; to see more detail, Alt+left-click (or Option+left-click on a Mac) the graph to pan across it and Alt+right-click to scale it (these are the same controls as navigating in the Scene view). Use the arrows on top of the shaded area to shrink it to under 4 milliseconds for all three Jump transitions.
最后,您可以通过一次选择一个动画节点并调整过渡顺序来完善动画网络。检查器将显示该节点的所有过渡列表;您可以拖动列表中的项目(它们的拖动手柄是左侧的图标)来重新排序它们。确保 Jump 过渡在 Idle 和 Run 节点的顶部,以便 Jump 过渡优先于其他过渡。
Finally, you can perfect the animation network by selecting the animation nodes one at a time and adjusting the ordering of transitions. The Inspector will show a list of all transitions to and from that node; you can drag items in the list (their drag handles are the icon on the left side) to reorder them. Make sure the Jump transition is on top for both the Idle and Run nodes so that the Jump transition has priority over the other transitions.
在查看这些设置时,如果动画看起来太慢,您还可以更改播放速度(以 1.5 倍速度运行效果会更好)。动画控制器已设置好,因此现在您可以从运动中操作动画脚本。
While you’re looking at these settings, you can also change the playback speed if the animation looks too slow (Run looks better at 1.5 speed). The animator controller is set up, so now you can operate the animations from the movement script.
最后,您将向RelativeMovement脚本添加方法。如前所述,设置动画状态的大部分工作都是在动画控制器中完成的;只需要少量代码即可操作丰富流畅的动画系统,如下所示。
Finally, you’ll add methods to the RelativeMovement script. As explained earlier, most of the work of setting up animation states is done in the animator controller; only a small amount of code is needed to operate a rich and fluid animation system, shown here.
Listing 8.6 Setting values in the Animator component
... 私人动画师动画师; ... animator = GetComponent<Animator>(); ❶ ... animator.SetFloat("速度", movement.sqrMagnitude); ❷ 如果(击中地面){ 如果(Input.GetButtonDown("跳转")){ 垂直速度 = 跳跃速度; } 别的 { vertSpeed = 最小跌落; animator.SetBool("跳跃",false); } } 别的 { vertSpeed += 重力 * 5 * Time.deltaTime; 如果 (垂直速度 < 终端速度) { 垂直速度 = 终端速度; } 如果 (联系人 != null ) { ❸ animator.SetBool("跳跃", true); } 如果(charController.isGrounded){ 如果(Vector3.Dot(运动,接触.正常)<0){ 运动 = 接触.正常 * 移动速度; } 别的 { 运动+=接触.正常*移动速度; } } } ...
... private Animator animator; ... animator = GetComponent<Animator>(); ❶ ... animator.SetFloat("Speed", movement.sqrMagnitude); ❷ if (hitGround) { if (Input.GetButtonDown("Jump")) { vertSpeed = jumpSpeed; } else { vertSpeed = minFall; animator.SetBool("Jumping", false); } } else { vertSpeed += gravity * 5 * Time.deltaTime; if (vertSpeed < terminalVelocity) { vertSpeed = terminalVelocity; } if (contact != null ) { ❸ animator.SetBool("Jumping", true); } if (charController.isGrounded) { if (Vector3.Dot(movement, contact.normal) < 0) { movement = contact.normal * moveSpeed; } else { movement += contact.normal * moveSpeed; } } } ...
❶ Added inside the Start() function
❷ Just below the entire if statement for horizontal movement
❸ Don’t trigger this value right at the beginning of the level.
同样,此清单中的大部分内容与之前的清单重复;动画代码是散布在现有动作脚本中的几行代码。挑选动画行以查找代码中需要添加的内容。
Again, much of this listing is repeated from previous listings; the animation code is a handful of lines interspersed throughout the existing movement script. Pick out the animator lines to find additions to make in your code.
脚本需要引用 Animator 组件,然后代码在动画器上设置值(浮点数或布尔值)。唯一不太明显的代码是设置Jumping布尔值之前的条件(contact != null)。该条件阻止动画器在游戏开始时播放跳跃动画。尽管角色在技术上只是一瞬间坠落,但在角色第一次接触地面之前不会生成任何碰撞数据。
The script needs a reference to the Animator component, and then the code sets values (either floats or Booleans) on the animator. The only somewhat nonobvious bit of code is the condition (contact != null) before setting the Jumping Boolean. That condition prevents the animator from playing the jump animation when the game starts. Even though the character is technically falling for a split second, no collision data is generated until the character touches the ground for the first time.
就这样!现在我们有了一个不错的第三人称移动演示,具有与相机相关的控制和角色动画片正在播放。
And there you have it! Now we have a nice third-person movement demo, with camera-relative controls and character animation playing.
Third-person view means the camera moves around the character instead of inside the character.
Simulated shadows, like real-time shadows and lightmaps, improve the graphics.
Controls can be relative to the camera instead of relative to the character.
You can improve on Unity’s ground detection by casting a ray downward.
Sophisticated animation set up with Unity’s animator controller results in lifelike characters.
实现功能性物品是我们要关注的下一个主题。前几章介绍了完整游戏的各种元素:运动、敌人、用户界面等等。但我们的项目除了敌人之外,还缺少任何可以互动的东西,也没有太多的游戏状态。在本章中,您将学习如何创建像门这样的功能性设备。
Implementing functional items is the next topic we’re going to focus on. Previous chapters covered various elements of a complete game: movement, enemies, the UI, and so forth. But our projects have lacked anything to interact with other than enemies, nor have they had much in the way of game state. In this chapter, you’ll learn how to create functional devices like doors.
我们还将讨论收集物品,这涉及与关卡中的对象交互以及跟踪游戏状态。游戏通常必须跟踪玩家当前状态,例如玩家的当前统计数据、目标进度等。玩家的库存就是这种状态的一个例子,因此您将构建一个可以跟踪玩家收集的物品的代码架构。在本章结束时,您将构建一个真正像游戏一样的动态空间!
We’ll also discuss collecting items, which involves both interacting with objects in the level and tracking game state. Games often have to track state like the player’s current stats, progress through objectives, and so on. The player’s inventory is an example of this sort of state, so you’ll build a code architecture that can keep track of items collected by the player. By the end of this chapter, you’ll have built a dynamic space that really feels like a game!
我们将首先探索通过玩家按键操作的设备(例如门)。之后,您将编写代码来检测玩家何时与关卡中的物体发生碰撞,从而实现诸如推动物体或收集库存物品等交互。然后,您将设置一个强大的模型视图控制器 (MVC) 样式的代码架构来管理收集的库存数据。最后,您将编写界面以利用库存进行游戏,例如需要钥匙才能打开门。
We’ll start by exploring devices (such as doors) that are operated with keypresses from the player. After that, you’ll write code to detect when the player collides with objects in the level, enabling interactions like pushing objects around or collecting inventory items. Then you’ll set up a robust Model-View-Controller (MVC) style of code architecture to manage data for the collected inventory. Finally, you’ll program interfaces to make use of the inventory for gameplay, such as requiring a key to open a door.
警告:前几章相对独立,从技术上讲不需要前面章节的项目,但这次一些代码清单对第 8 章的脚本进行了编辑。如果您直接跳到本章,请下载第 8 章的示例项目以在此基础上进行构建。
WARNING Previous chapters were relatively self-contained and didn’t technically require projects from earlier chapters, but this time some of the code listings make edits to scripts from chapter 8. If you skipped directly to this chapter, download the sample project for chapter 8 to build on that.
示例项目会将这些设备和物品随机散布在关卡中。一款精致的游戏会在物品的放置背后进行大量精心设计,但我们不需要仔细规划仅用于测试功能的关卡。然而,即使物体的放置不需要计划,本章开头的要点也会列出我们将实现事物的顺序。像往常一样,解释会逐步构建代码,但如果您想在一个地方查看所有完成的代码,您可以下载示例项目。
The example project will have these devices and items randomly strewn about the level. A polished game would have a lot of careful design behind the placement of items, but we don’t need to carefully plan out a level that only tests functionality. However, even though the placement of objects doesn’t require a plan, the bullet points at the start of the chapter lay out the order in which we’ll implement things. As usual, the explanations build up the code step by step, but if you want to see all the finished code in one place, you can download the sample project.
虽然游戏中的关卡主要由静态墙壁和场景组成,它们通常还包含许多功能设备。我指的是玩家可以与之交互和操作的对象——例如打开的灯或开始转动的风扇。具体设备可能千差万别,而且大多只受你的想象力限制,但几乎所有设备都使用相同类型的代码来让玩家激活设备。你将在本章中实现几个示例,然后你应该能够调整相同的代码以与各种其他设备配合使用。
Although levels in games consist mostly of static walls and scenery, they also usually incorporate a lot of functional devices. I’m talking about objects that the player can interact with and operate—things like lights that turn on or a fan that starts turning. The specific devices can vary a lot and are mostly limited only by your imagination, but almost all of them use the same sort of code to have the player activate the device. You’ll implement a couple of examples in this chapter, and then you should be able to adapt this same code to work with all sorts of other devices.
这您要编程的第一种设备是可以打开和关闭的门,您将首先通过按下按键来操作门。游戏中可以有很多设备,并且有很多种方法来操作这些设备。我们最终将研究几种变体,但门是游戏中最常见的交互式设备,使用按键式物品是最简单的入门方法。
The first kind of device you’ll program is a door that opens and closes, and you’re going to start with operating the door by pressing a key. You could have lots of devices in a game, and lots of ways to operate those devices. We’re eventually going to look at a couple of variations, but doors are the most common interactive devices found in games, and using items with a keypress is the most straightforward approach to start with.
场景中有几个地方墙壁之间有缝隙,因此需要放置一个新物体来遮挡缝隙。我创建了一个新的立方体对象,然后将其变换设置为位置2.5、1.5、17和比例5、3、0.5,从而创建如图 9.1 所示的门。
The scene has a few spots where a gap exists between walls, so place a new object that blocks the gap. I created a new cube object and then set its transform to Position 2.5, 1.5, 17 and Scale 5, 3, 0.5, creating the door shown in figure 9.1.
Figure 9.1 Door object fit into a gap in the wall
创建一个 C# 脚本,将其命名为DoorOpenDevice,并将该脚本放在门对象上。此代码将使该对象作为门运行。
Create a C# script, call it DoorOpenDevice, and put that script on the door object. This code will cause the object to operate as a door.
Listing 9.1 Script that opens and closes the door on command
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 DoorOpenDevice : MonoBehaviour {
[SerializeField] Vector3 dPos; ❶
私有 bool 打开; ❷
公共无效操作(){
如果(打开){ ❸
向量3 pos = 变换.位置- dPos;
变换.位置 = pos;
} 别的 {
向量3 pos = 变换.位置 + dPos;
变换.位置 = pos;
}
打开 = !打开;
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DoorOpenDevice : MonoBehaviour {
[SerializeField] Vector3 dPos; ❶
private bool open; ❷
public void Operate() {
if (open) { ❸
Vector3 pos = transform.position - dPos;
transform.position = pos;
} else {
Vector3 pos = transform.position + dPos;
transform.position = pos;
}
open = !open;
}
}
❶ Amount to offset the position by when the door opens
❷ Boolean to keep track of the open state of the door
❸ Open or close the door depending on the open state.
第一个变量定义门打开时应用的偏移量。门打开时将移动此量,然后关闭时将减去此量。第二个变量是私有布尔值,用于跟踪门是打开还是关闭。在Operate()方法中,对象的变换被设置为一个新的位置,根据门是否已经打开来增加或减少偏移量;然后打开或关闭。
The first variable defines the offset that’s applied when the door opens. The door will move this amount when it opens, and then it will subtract this amount when it closes. The second variable is a private Boolean for tracking whether the door is open or closed. In the Operate() method, the object’s transform is set to a new position, adding or subtracting the offset depending on whether the door is already open; then open is toggled on or off.
与其他序列化变量一样,dPos出现在 Inspector 中。但这是一个Vector3值,因此我们有三个输入框,而不是一个,所有输入框都位于一个变量名下。输入门打开时的相对位置;我决定让门向下滑动打开,因此偏移量为0,-2.9,0(因为门对象的高度为 3 ,向下移动 2.9 会使门的一小块突出地板)。
As with other serialized variables, dPos appears in the Inspector. But this is a Vector3 value, so instead of one input box, we have three, all under the one variable name. Type in the relative position of the door when it opens; I decided to have the door slide down to open, so the offset is 0, -2.9, 0 (because the door object has a height of 3, moving down 2.9 leaves a tiny sliver of the door sticking up out of the floor).
注意:变换会立即应用,但您可能更喜欢看到门打开时的运动。如第 3 章所述,您可以使用补间使对象随时间平滑移动。补间一词在不同上下文中含义不同,但在游戏编程中,它指的是导致对象移动的代码命令;附录 D 提到了 Unity 的补间系统。
NOTE The transform is applied instantly, but you may prefer seeing the movement when the door opens. As mentioned in chapter 3, you can use tweens to make objects move smoothly over time. The word tween means different things in different contexts, but in game programming it refers to code commands that cause objects to move around; appendix D mentions tweening systems for Unity.
其他代码需要调用Operate()来打开和关闭门(单个函数调用处理两种情况)。您还没有在播放器上安装其他脚本,因此下一步就是编写该脚本步。
Other code needs to call Operate() to make the door open and close (the single function call handles both cases). You don’t yet have that other script on the player, so writing that is the next step.
创造新建一个脚本,命名为DeviceOperator。这个清单实现了操作附近设备的控制键。
Create a new script and name it DeviceOperator. This listing implements a control key that operates nearby devices.
Listing 9.2 Device control key for the player
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 DeviceOperator : MonoBehaviour {
公共浮动半径 = 1.5f; ❶
无效更新(){
if (Input.GetKeyDown(KeyCode.C)) { ❷
对撞机[] hitColliders =
物理.重叠球体(变换.位置,半径); ❸
foreach (碰撞器 hitCollider 在 hitColliders 中) {
hitCollider.SendMessage("操作",
SendMessageOptions.DontRequireReceiver); ❹
}
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DeviceOperator : MonoBehaviour {
public float radius = 1.5f; ❶
void Update() {
if (Input.GetKeyDown(KeyCode.C)) { ❷
Collider[] hitColliders =
Physics.OverlapSphere(transform.position, radius); ❸
foreach (Collider hitCollider in hitColliders) {
hitCollider.SendMessage("Operate",
SendMessageOptions.DontRequireReceiver); ❹
}
}
}
}
❶ How far away from the player to activate devices
❷ Respond when the named key is pressed down.
❸ OverlapSphere() returns a list of nearby objects.
❹ SendMessage() 尝试调用命名函数,无论目标的类型如何。
❹ SendMessage() tries to call the named function, regardless of the target’s type.
此清单的大部分内容看起来应该很熟悉,但核心是一个关键的新方法。首先,确定操作设备的距离值。然后,在Update()函数中,寻找键盘输入。就像RelativeMovement脚本一样使用GetButtonDown()和项目输入设置中的按钮,这次您将使用GetKeyDown()进行特定字母键的输入。
The majority of this listing should look familiar, but a crucial new method is at the center. First, establish a value for how far away to operate devices from. Then, in the Update() function, look for keyboard input. Just as the RelativeMovement script uses GetButtonDown() and a button from the project’s input settings, this time you’ll use GetKeyDown() for input from a specific letter key.
现在我们来谈谈关键的新方法:OverlapSphere()。此方法返回给定位置给定距离内的所有对象的数组。通过传入玩家的位置和半径变量,此方法检测玩家附近的所有物体。您对此列表的操作可能有所不同(也许您引爆了炸弹并想要施加爆炸力),但在这种情况下,您需要尝试对所有附近的物体调用Operate() 。
Now we get to the crucial new method: OverlapSphere(). This method returns an array of all objects that are within a given distance of a given position. By passing in the position of the player and the radius variable, this method detects all objects near the player. What you do with this list can vary (perhaps you set off a bomb and want to apply an explosive force), but in this situation you want to attempt to call Operate() on all nearby objects.
该方法是通过SendMessage()调用的,而不是典型的点符号,这种方法在前面的章节中也曾使用过。与那里的情况一样,您使用SendMessage()是因为您不知道目标对象的确切类型,并且该命令适用于所有 GameObject。但这次您要将DontRequireReceiver选项传递给该方法。这是因为OverlapSphere()返回的大多数对象都没有Operate()方法;通常,如果对象中没有任何对象收到消息, SendMessage()会打印一条错误消息,但在这种情况下,错误消息会分散注意力,因为您已经知道大多数对象会忽略该消息。
That method is called via SendMessage() instead of the typical dot notation, an approach you also saw with UI buttons in previous chapters. As was the case there, you use SendMessage() because you don’t know the exact type of the target object, and that command works on all GameObjects. But this time you’re going to pass the DontRequireReceiver option to the method. This is because most of the objects returned by OverlapSphere() won’t have an Operate() method; normally, SendMessage() prints an error message if nothing in the object received the message, but in this case the error messages would be distracting because you already know most objects will ignore the message.
代码写好后,你可以把这个脚本附加到玩家对象上。现在你站在门边按下钥匙就可以打开和关闭门了。
Once the code is written, you can attach this script to the player object. Now you can open and close the door by standing near it and pressing the key.
您可以修复一个小细节。目前,只要玩家距离足够近,玩家面向哪个方向都无关紧要。但您也可以调整脚本以仅操作玩家面向的设备,所以让我们这样做。回想一下第 8 章,您可以计算点积来检查朝向。这是对一对向量执行的数学运算,返回-N和N之间的范围,其中N表示它们指向完全相同的方向,而当它们指向完全相反的方向时则为-N。好吧,当向量被标准化时, N为 1,从而得到一个易于操作的范围,从 -1 到 1。
You can fix one little detail. Currently, it doesn’t matter which way the player is facing, as long as the player is close enough. But you could also adjust the script to operate only devices the player is facing, so let’s do that. Recall from chapter 8 that you can calculate the dot product for checking facing. That’s a mathematical operation done on a pair of vectors that returns a range between -N and N, with N meaning they point in exactly the same direction and -N when they point in exactly opposite directions. Well, N is 1 when the vectors are normalized, resulting in an easy-to-work-with range from -1 to 1.
定义当向量被归一化时,结果继续指向同一方向,但其长度(也称为其幅度)被调整为 1。许多数学运算最适合使用规范化向量,因此 Unity 提供了返回规范化向量的属性。
Definition When a vector is normalized, the result continues to point in the same direction, but its length (also referred to as its magnitude) is adjusted to 1. Many mathematical operations work best with normalized vectors, so Unity provides properties that return normalized vectors.
Here is the new code in the DeviceOperator script.
清单 9.3 调整DeviceOperator以仅操作玩家面对的设备
Listing 9.3 Adjusting DeviceOperator to operate only devices that the player faces
...
foreach (碰撞器 hitCollider 在 hitColliders 中) {
Vector3 hitPosition = hitCollider.transform.position;
hitPosition.y = transform.position.y; ❶
Vector3 方向 = hitPosition - transform.position;
如果 (Vector3.Dot(transform.forward, direction.normalized) > .5f) { ❷
hitCollider.SendMessage("操作",
发送消息选项.不需要接收者);
}
}
......
foreach (Collider hitCollider in hitColliders) {
Vector3 hitPosition = hitCollider.transform.position;
hitPosition.y = transform.position.y; ❶
Vector3 direction = hitPosition - transform.position;
if (Vector3.Dot(transform.forward, direction.normalized) > .5f) { ❷
hitCollider.SendMessage("Operate",
SendMessageOptions.DontRequireReceiver);
}
}
...
❶ Vertical correction so the direction won’t point up or down
❷ Send the message only when facing the right direction.
要使用点积,首先要确定要检查的方向。那就是从玩家到物体的方向;通过从物体的位置减去玩家的位置来生成一个方向向量(校正垂直位置,这样方向将是水平的,而不是指向降低的门)。然后使用该方向向量和玩家的前进方向调用Vector3.Dot()。当点积接近 1 时(具体来说,此代码检查它是否大于 0.5),两个向量接近指向同一方向。
To use the dot product, you first determine the direction to check against. That would be the direction from the player to the object; make a direction vector by subtracting the position of the player from the position of the object (with the vertical position corrected, so that the direction will be horizontal instead of pointing down at the lowered door). Then call Vector3.Dot() with both that direction vector and the forward direction of the player. When the dot product is close to 1 (specifically, this code checks whether it is greater than 0.5), the two vectors are close to pointing in the same direction.
经过这一调整后,即使玩家离门很近,门也不会在玩家背对门时打开和关闭。这种操作设备的方法可以用于任何类型的设备。为了展示这种灵活性,让我们创建另一个示例设备。
With this adjustment made, the door won’t open and close when the player faces away from it, even if the player is close. And this same approach to operating devices can be used with any sort of device. To demonstrate that flexibility, let’s create another example device.
我们创建了一扇可以打开和关闭的门,但相同的设备操作逻辑可以用于任何类型的设备。您将创建另一个以相同方式操作的设备;这一次,您将在墙上创建一个变色显示屏。
We’ve created a door that opens and closes, but that same device-operating logic can be used with any sort of device. You’re going to create another device that’s operated in the same way; this time, you’ll create a color-changing display on the wall.
创建一个新的立方体,并将其放置在一侧刚好露出墙面的位置。例如,我选择了位置10.9、1.5、-5。现在创建一个名为ColorChangeDevice的新脚本,并将该脚本(清单 9.4)附加到墙面显示器。跑到墙面显示器前,按下与门相同的“操作”键;您应该会看到显示屏改变颜色,如图 9.2 所示。
Create a new cube and place it so that one side is barely sticking out of the wall. For example, I went with Position 10.9, 1.5, -5. Now create a new script called ColorChangeDevice and attach that script (listing 9.4) to the wall display. Run up to the wall monitor and press the same “operate” key as used with the door; you should see the display change color, as figure 9.2 illustrates.
Figure 9.2 Color-changing display embedded in the wall
Listing 9.4 Script for a device that changes color
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 ColorChangeDevice : MonoBehaviour {
公共无效操作(){ ❶
颜色随机 = 新颜色(Random.Range(0f,1f),
随机范围(0f,1f),随机范围(0f,1f)); ❷
GetComponent <Renderer>()。material.color = random; ❸
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ColorChangeDevice : MonoBehaviour {
public void Operate() { ❶
Color random = new Color(Random.Range(0f,1f),
Random.Range(0f,1f), Random.Range(0f,1f)); ❷
GetComponent<Renderer>().material.color = random; ❸
}
}
❶ Declare a method with the same name as the door script.
❷ The numbers are RGB values that range from 0 to 1.
❸ The color is set in the material attached to the object.
首先,声明与使用的门脚本相同的函数名。操作是设备操作员脚本使用的函数名称,因此您需要使用该名称才能触发它。在此函数中,代码为对象的材质分配随机颜色(请记住,颜色不是对象本身的属性,而是对象具有材质,并且该材质可以具有颜色)。
To start with, declare the same function name as the door script used. Operate is the function name that the device operator script uses, so you need to use that name for it to be triggered. Inside this function, the code assigns a random color to the object’s material (remember, color isn’t an attribute of the object itself, but rather the object has a material, and that material can have a color).
注意:尽管颜色是由红色、蓝色和绿色分量定义的,这是大多数计算机图形学中的标准,但 Unity 的颜色对象中的值在0和1之间变化,而不是0和255之间变化,这在大多数地方很常见(包括 Unity 的颜色选择器 UI)。
NOTE Although the color is defined with Red, Blue, and Green components, as is standard in most computer graphics, the values in Unity’s Color object vary between 0 and 1, instead of 0 and 255, as is common in most places (including Unity’s color picker UI).
好了,我们已经介绍了一种与游戏中的设备交互的方法,甚至还实现了几个设备来演示。另一种与物品交互的方式是撞击它们,让我们来看看那下一个。
All right, so we’ve gone over one approach to interacting with devices in the game and have even implemented a couple of devices to demonstrate. Another way of interacting with items is by bumping into them, so let’s go over that next.
在在上一节中,设备由玩家的键盘输入操作,但这并不是玩家与关卡中的物品互动的唯一方式。另一种直接的方法是响应玩家的碰撞。Unity 通过在游戏引擎中内置碰撞检测和物理来为您处理大部分工作。Unity 将为您检测碰撞,但您仍需要对对象进行编程以做出响应。
In the previous section, devices were operated by keyboard input from the player, but that’s not the only way players can interact with items in the level. Another straightforward approach is to respond to collisions with the player. Unity handles most of that for you, by having collision detection and physics built into the game engine. Unity will detect collisions for you, but you still need to program the object to respond.
We’ll go over three collision responses that are useful for games:
到首先,您要创建一堆箱子,然后当玩家撞到箱子时使箱子倒塌。虽然其中涉及的物理计算很复杂,但 Unity 已内置了所有这些功能,并将以逼真的方式散落箱子。
To start, you’re going to create a pile of boxes and then cause the pile to collapse when the player runs into it. Although the physics calculations involved are complicated, Unity has all of that built in and will scatter the boxes in a realistic way.
默认情况下,Unity 不使用物理模拟来移动物体。可以通过向物体添加 Rigidbody 组件来启用此功能。这个概念最初在第 3 章中讨论过,因为敌人的火球也需要 Rigidbody 组件。正如我在该章中解释的那样,Unity 的物理系统只对具有 Rigidbody 组件的物体起作用。单击“添加组件”并转到“物理”(不是“物理 2D”)菜单,查找 Rigidbody。
By default, Unity doesn’t use its physics simulation to move objects around. That can be enabled by adding a Rigidbody component to the object. This concept was first discussed in chapter 3, because the enemy’s fireballs also needed a Rigidbody component. As I explained in that chapter, Unity’s physics system will act only on objects that have a Rigidbody component. Look for Rigidbody by clicking Add Component and going to the Physics (not Physics 2D!) menu.
创建一个新的立方体对象,然后为其添加 Rigidbody 组件。创建几个这样的立方体,并将它们整齐地堆叠在一起。例如,在示例下载中,我创建了五个盒子,并将它们堆叠成两层(见图 9.3)。
Create a new cube object and then add a Rigidbody component to it. Create several such cubes and position them in a neat stack. For example, in the sample download, I created five boxes and stacked them into two tiers (see figure 9.3).
Figure 9.3 Stack of five boxes to collide with
现在盒子已经准备好对物理力做出反应了。要让玩家对盒子施加力,请对玩家的RelativeMovement脚本(这是第 8 章中编写的脚本之一)进行以下列表中的小添加。
The boxes are now ready to react to physics forces. To have the player apply a force to the boxes, make the small addition shown in the following listing to the RelativeMovement script (this is one of the scripts written in chapter 8) that’s on the player.
清单 9.5 向RelativeMovement脚本添加物理力
Listing 9.5 Adding physics force to the RelativeMovement script
... 公共浮动 pushForce = 3.0f; ❶ ... void OnControllerColliderHit(ControllerColliderHit hit) { 接触 = 命中; Rigidbody body = hit.collider.attachedRigidbody; ❷ 如果 (body != null && !body.isKinematic) { body.velocity = hit.moveDirection * pushForce; ❸ } } ...
... public float pushForce = 3.0f; ❶ ... void OnControllerColliderHit(ControllerColliderHit hit) { contact = hit; Rigidbody body = hit.collider.attachedRigidbody; ❷ if (body != null && !body.isKinematic) { body.velocity = hit.moveDirection * pushForce; ❸ } } ...
❷ Check if the collided object has a Rigidbody to receive physics forces.
❸ Apply velocity to the physics body.
这段代码没什么好解释的:每当玩家与某物发生碰撞时,检查碰撞物体是否具有 Rigidbody 组件。如果有,则向该 Rigidbody 施加速度。
There’s not much to explain about this code: whenever the player collides with something, check whether the collided object has a Rigidbody component. If so, apply a velocity to that Rigidbody.
玩游戏,然后跑进一堆箱子里;你应该看到它们真实地散落一地。这就是你在场景中激活一堆箱子上的物理模拟所要做的全部工作!Unity 内置了物理模拟,因此你不必编写太多代码。该模拟可能会导致物体在碰撞时移动,但另一种可能的响应是触发触发事件,因此让我们使用这些触发事件来控制门。
Play the game and then run into the pile of boxes; you should see them scatter around realistically. And that’s all you have to do to activate physics simulation on a stack of boxes in the scene! Unity has physics simulation built in, so you don’t have to write much code. That simulation can cause objects to move around in response to collisions, but another possible response is firing trigger events, so let’s use those trigger events to control the door.
之前,门是通过按键操作的。这一次,它会根据角色与场景中的另一个物体的碰撞而打开和关闭。
Previously, the door was operated by a keypress. This time it will open and close in response to the character colliding with another object in the scene.
创建另一扇门并将其放置在另一个墙壁间隙中(我复制了之前的门并将新门移动到-2.5、1.5、-17)。现在创建一个新的立方体作为触发器对象,并为碰撞器选中 Is Trigger 复选框(此步骤在第 3 章制作火球时进行了说明)。此外,将触发器对象设置为 Ignore Raycast 层;检查器右上角有一个 Layer 菜单。最后,您应该关闭此对象的 Cast Shadows(请记住,当您选择对象时,此设置位于 Mesh Renderer 下)。
Create yet another door and place it in another wall gap (I duplicated the previous door and moved the new door to -2.5, 1.5, -17). Now create a new cube to use for the trigger object, and select the Is Trigger check box for the collider (this step was illustrated when making the fireball in chapter 3). In addition, set the trigger object to the Ignore Raycast layer; the top-right corner of the Inspector has a Layer menu. Finally, you should turn off Cast Shadows from this object (remember, this setting is under Mesh Renderer when you select the object).
警告这些微小的步骤很容易被忽略,但很重要:要使用对象作为触发器,请务必打开 Is Trigger。在 Inspector 中,查找 Collider 组件中的复选框。此外,将图层更改为 Ignore Raycast,这样触发器对象就不会出现在光线投射中。
WARNING These tiny steps are easy to miss but important: To use an object as a trigger, be sure to turn on Is Trigger. In the Inspector, look for the check box in the Collider component. Also, change the layer to Ignore Raycast so that the trigger object won’t show up in raycasting.
注意在第 3 章中引入触发器对象时,该对象需要添加 Rigidbody 组件。这次触发器不需要 Rigidbody,因为触发器将响应玩家(而不是与墙壁碰撞,这是之前的情况)。要使触发器正常工作,触发器或进入触发器的对象都需要启用 Unity 的物理系统;Rigidbody 组件满足此要求,但玩家的角色控制器也满足此要求。
NOTE When trigger objects were introduced in chapter 3, the object needed to have a Rigidbody component added. Rigidbody isn’t required for the trigger this time because the trigger will be responding to the player (versus colliding with a wall, the earlier situation). For triggers to work, either the trigger or the object entering the trigger needs to have Unity’s physics system enabled; a Rigidbody component fulfills this requirement, but so does the player’s character controller.
定位和缩放触发器对象,使其既包含门又围绕门周围的区域;我使用了位置-2.5、1.5、-17(与门相同)和比例7.5、3、6。此外,您可能希望为对象分配半透明材质,以便您可以在视觉上区分触发器体积和固体对象。使用 Assets 菜单创建新材质,然后在 Project 视图中选择新材质。查看 Inspector,顶部设置是渲染模式(当前设置为默认值不透明);在此菜单中选择透明。
Position and scale the trigger object so that it both encompasses the door and surrounds an area around the door; I used Position -2.5, 1.5, -17 (same as the door) and Scale 7.5, 3, 6. Additionally, you may want to assign a semitransparent material to the object so that you can visually distinguish trigger volumes from solid objects. Create a new material by using the Assets menu, and select the new material in the Project view. Looking at the Inspector, the top setting is Rendering Mode (currently set to the default value of Opaque); select Transparent in this menu.
现在单击 Albedo 颜色样本以调出颜色选择器窗口。在窗口的主要部分中选择绿色,然后使用底部滑块降低 alpha。将此材质从 Project 拖到对象上;图 9.4 显示了使用此材质的触发器。
Now click the Albedo color swatch to bring up the Color Picker window. Pick green in the main part of the window, and lower the alpha by using the bottom slider. Drag this material from Project onto the object; figure 9.4 shows the trigger with this material.
Figure 9.4 Trigger volume surrounding the door it will trigger
定义 触发器通常被称为体积而不是物体,以从概念上区分实体物体和可以穿过的物体。
DEFINITION Triggers are often referred to as volumes rather than objects to conceptually differentiate solid objects from objects you can move through.
现在玩游戏,您可以自由地穿过触发器体积。 Unity 仍会记录与对象的碰撞,但这些碰撞不再影响玩家的移动。 要对碰撞做出反应,您需要编写代码。 具体来说,您希望此触发器控制门。 创建一个名为DeviceTrigger的新脚本。
Play the game now and you can freely move through the trigger volume. Unity still registers collisions with the object, but those collisions don’t affect the player’s movement anymore. To react to the collisions, you need to write code. Specifically, you want this trigger to control the door. Create a new script called DeviceTrigger.
Listing 9.6 Code for a trigger that controls a device
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 DeviceTrigger : MonoBehaviour {
[SerializeField] GameObject[] 目标; ❶
void OnTriggerEnter(Collider 其他) { ❷
foreach(目标中的游戏对象目标){
目标.发送消息(“激活”);
}
}
void OnTriggerExit(Collider other) { ❸
foreach(目标中的游戏对象目标){
目标.发送消息(“停用”);
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DeviceTrigger : MonoBehaviour {
[SerializeField] GameObject[] targets; ❶
void OnTriggerEnter(Collider other) { ❷
foreach (GameObject target in targets) {
target.SendMessage("Activate");
}
}
void OnTriggerExit(Collider other) { ❸
foreach (GameObject target in targets) {
target.SendMessage("Deactivate");
}
}
}
❶ List of target objects that this trigger will activate
❷当另一个物体进入触发体积时,将调用 OnTriggerEnter()……
❷ OnTriggerEnter() is called when another object enters the trigger volume . . .
❸ . . .而当对象离开触发体积时,会调用OnTriggerExit()。
❸ . . . whereas OnTriggerExit() is called when an object leaves the trigger volume.
此列表定义了触发器的目标对象数组;尽管大多数情况下它只是一个列表,但有可能由单个触发器控制多个设备。循环遍历目标数组以向所有目标发送消息。此循环发生在 OnTriggerEnter内部()和OnTriggerExit()方法。当另一个对象第一次进入和退出触发器时,这些函数会被调用一次(而不是当对象在触发器体积内时一遍又一遍地调用)。
This listing defines an array of target objects for the trigger; even though it’ll be a list of only one most of the time, it’s possible to have multiple devices controlled by a single trigger. Loop through the array of targets to send a message to all the targets. This loop happens inside the OnTriggerEnter() and OnTriggerExit() methods. These functions are called once when another object first enters and exits the trigger (as opposed to being called over and over while the object is inside the trigger volume).
请注意,发送的消息与之前不同;现在您需要定义激活()和Deactivate()函数在门上。将下面列出的代码添加到DoorOpenDevice脚本中。
Notice that the messages being sent are different from before; now you need to define the Activate() and Deactivate() functions on the door. Add the code in the next listing to the DoorOpenDevice script.
清单 9.7 向DoorOpenDevice脚本添加激活和停用函数
Listing 9.7 Adding activate and deactivate functions to the DoorOpenDevice script
...
公共无效激活(){
如果(!打开){ ❶
向量3 pos = 变换.位置 + dPos;
变换.位置 = pos;
打开=真;
}
}
公共无效停用(){
如果(打开){ ❷
向量3 pos = 变换.位置- dPos;
变换.位置 = pos;
打开=假;
}
}
......
public void Activate() {
if (!open) { ❶
Vector3 pos = transform.position + dPos;
transform.position = pos;
open = true;
}
}
public void Deactivate() {
if (open) { ❷
Vector3 pos = transform.position - dPos;
transform.position = pos;
open = false;
}
}
...
❶ Open the door only if it isn’t already open.
❷ Close the door only if it isn’t already closed.
新的Activate()和Deactivate()方法与之前的Operate()方法的代码非常相似,只是现在有单独的函数来打开和关闭门,而不是只有一个函数来处理两种情况。
The new Activate() and Deactivate() methods are much the same code as the Operate() method from earlier, except now separate functions open and close the door instead of only one function that handles both cases.
编写完所有必要的代码后,您现在可以使用触发器音量来打开和关闭门。将DeviceTrigger脚本然后将门链接到目标属性该脚本的;在 Inspector 中,首先设置数组的大小,然后将对象从 Hierarchy 视图拖到目标数组中的插槽上。由于您只有一个门想要用这个触发器控制,因此请在数组的 Size 字段中输入1,然后将该门拖到目标插槽中。
With all the necessary code in place, you can now use the trigger volume to open and close the door. Put the DeviceTrigger script on the trigger volume and then link the door to the targets property of that script; in the Inspector, first set the size of the array and then drag objects from the Hierarchy view over to slots in the targets array. Because you have only one door that you want to control with this trigger, type 1 in the array’s Size field and then drag that door into the target slot.
完成所有这些操作后,玩游戏并观察玩家走向和离开门时门会发生什么情况。当玩家进入和离开触发体积时,门会自动打开和关闭。
With all of this done, play the game and watch what happens to the door when the player walks toward and away from it. It’ll open and close automatically as the player enters and leaves the trigger volume.
这是将互动性融入关卡的另一种好方法!但这种触发量方法不仅适用于门等设备;您还可以使用此方法制作收藏品项目。
That’s another great way to put interactivity into levels! But this trigger volume approach doesn’t work only with devices like doors; you can also use this approach to make collectible items.
许多游戏中包括玩家可以拾取的物品。这些物品包括装备、健康包和能量增强道具。与物品碰撞并拾取它们的基本机制很简单;大多数复杂的事情发生在物品被拾取之后,但我们稍后会讲到这一点。
Many games include items that can be picked up by the player. These items include equipment, health packs, and power-ups. The basic mechanism of colliding with items to pick them up is simple; most of the complicated stuff happens after items are picked up, but we’ll get to that a bit later.
创建一个球体物体,并将其悬停在场景中开阔区域的腰部高度左右。将物体缩小(如 Scale 0.5、0.5、0.5),但其他准备工作与大触发器体积相同。选择对撞机中的 Is Trigger 设置,将物体设置为 Ignore Raycast 层,然后创建新材质以赋予物体独特的颜色。由于物体覆盖的范围不大,因此无需将其设为半透明,因此这次不要调低 alpha 滑块。此外,如第 8 章所述,可以使用设置来移除此物体投射的阴影;是否使用阴影是一个判断问题,但对于像这样的小型拾取物品,我更喜欢关闭它们。
Create a sphere object and place it hovering at about waist height in an open area of the scene. Make the object small (like Scale 0.5, 0.5, 0.5), but otherwise prepare it as you did with the large trigger volume. Select the Is Trigger setting in the collider, set the object to the Ignore Raycast layer, and then create a new material to give the object a distinct color. Because the object doesn’t cover much, you don’t need to make it semitransparent, so don’t turn down the alpha slider this time. Also, as mentioned in chapter 8, settings are available for removing the shadows cast from this object; whether to use the shadows is a judgment call, but for small pickup items like this, I prefer to turn them off.
现在场景中的对象已准备就绪,请创建一个新脚本以附加到该对象。将脚本命名为CollectibleItem。
Now that the object in the scene is ready, create a new script to attach to that object. Call the script CollectibleItem.
Listing 9.8 Script that makes an item delete itself on contact with the player
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 CollectibleItem : MonoBehaviour {
[SerializeField] string itemName; ❶
void OnTriggerEnter(Collider 其他){
Debug.Log($"已收集物品:{itemName}");
销毁(this.gameObject);
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CollectibleItem : MonoBehaviour {
[SerializeField] string itemName; ❶
void OnTriggerEnter(Collider other) {
Debug.Log($"Item collected: {itemName}");
Destroy(this.gameObject);
}
}
❶ Type the name of this item in the Inspector.
这个脚本非常简短和简单。为项目指定一个名称值,以便不同的项目可以出现在场景中。OnTriggerEnter ()会自行销毁。还会将调试消息打印到控制台;最终它将被有用的代码替换。
This script is extremely short and simple. Give the item a name value so that different items can be in the scene. OnTriggerEnter()destroys itself. A debug message is also being printed to the console; eventually it will be replaced with useful code.
警告确保在this.gameObject上调用Destroy()而不是this!不要混淆这两者;this仅指此脚本组件,而this.gameObject指脚本附加到的对象。
WARNING Be sure to call Destroy() on this.gameObject and not this! Don’t get confused between the two; this refers only to this script component, whereas this.gameObject refers to the object the script is attached to.
回到 Unity,您添加到代码中的变量应该在 Inspector 中可见。输入一个名称来标识此项目;我为第一个项目选择了能量。然后复制该项目几次并更改副本的名称;我还创建了矿石、健康值和钥匙(这些名称必须准确,因为它们稍后将在代码中使用)。还要为每个项目创建单独的材料以赋予它们不同的颜色:我使用了浅蓝色能量、深灰色矿石、粉红色健康和黄色钥匙。
Back in Unity, the variable you added to the code should become visible in the Inspector. Type in a name to identify this item; I went with energy for my first item. Then duplicate the item a few times and change the name of the copies; I also created ore, health, and key (these names must be exact because they’ll be used in code later). Also create separate materials for each item to give them distinct colors: I used light blue energy, dark gray ore, pink health, and yellow key.
提示:正如我们在这里所做的那样,在更复杂的游戏中,物品通常有一个标识符,用于查找更多数据,而不是名称。例如,一个物品可能被分配 ID 301,而 ID 301 与某个显示名称、图像、描述等相关。
TIP Rather than a name, as we’ve done here, items in more complex games often have an identifier used to look up further data. For example, one item might be assigned ID 301, and ID 301 correlates to a certain display name, image, description, and so forth.
现在制作项目的预制件,以便您可以在整个关卡中克隆它们。在第 3 章中,我解释了将对象从层次结构视图向下拖动到项目视图会将该对象变成预制件;对所有四个项目都执行此操作。
Now make prefabs of the items so you can clone them throughout the level. In chapter 3, I explained that dragging an object from the Hierarchy view down to the Project view will turn that object into a prefab; do that for all four items.
注意:对象的名称将在层次结构列表中变为蓝色;蓝色名称表示对象是预制件的实例。右键单击预制件实例以选择“选择预制件”,然后选择对象所属的预制件。
NOTE The object’s name will turn blue in the Hierarchy list; blue names indicate objects that are instances of a prefab. Right-click a prefab instance to pick Select Prefab and select the prefab that the object is an instance of.
拖出预制件实例并将物品放置在关卡的开放区域中;甚至拖出同一物品的多个副本进行测试。玩游戏并遇到物品并收集它们。这很棒,但目前收集物品时什么都没有发生。您将开始跟踪收集的物品;为此,您需要设置库存代码结构。
Drag out instances of the prefabs and place the items in open areas of the level; even drag out multiple copies of the same item to test with. Play the game and run into items to collect them. That’s pretty neat, but at the moment nothing happens when you collect an item. You’re going to start keeping track of the items collected; to do that, you need to set up the inventory code structure.
现在编写了收集物品的功能后,您需要为游戏的库存编写后台数据管理器(类似于 Web 编码模式)。您编写的代码将类似于许多 Web 应用程序背后的 MVC 架构。这些数据管理器的优势在于将数据存储与屏幕上显示的对象分离,从而更轻松地进行实验和迭代开发。即使数据和/或显示很复杂,应用程序某一部分的更改也不会影响应用程序的其他部分。
Now that you’ve programmed the features of collecting items, you need background data managers (similar to web coding patterns) for the game’s inventory. The code you’ll write will be similar to the MVC architectures behind many web applications. The advantage of these data managers is in decoupling data storage from the objects that are displayed onscreen, allowing for easier experimentation and iterative development. Even when the data and/or displays are complex, changes in one part of the application don’t affect other parts of the application.
尽管如此,这种结构在不同的游戏中差异很大,因为并非每个游戏都有相同的数据管理需求。例如,角色扮演游戏的数据管理需求很高,因此您可能希望实现类似 MVC 架构的东西。但是,益智游戏需要管理的数据很少,因此构建复杂的解耦数据管理器结构会有些过头。相反,可以在特定于场景的控制器对象中跟踪游戏状态(事实上,这就是我们在前几章中处理游戏状态的方式)。
That said, such structures vary a lot among games, because not every game has the same data-management needs. For example, a role-playing game will have high data-management needs, so you probably want to implement something like an MVC architecture. A puzzle game, though, has little data to manage, so building a complex decoupled structure of data managers would be overkill. Instead, the game state can be tracked in the scene-specific controller objects (indeed, that’s how we handled game state in previous chapters).
在这个项目中,您需要管理玩家的库存。让我们设置所需的代码结构。
In this project, you need to manage the player’s inventory. Let’s set up the code structure needed for that.
这这里的一般思路是将所有数据管理拆分成单独的、定义明确的模块,每个模块管理自己的职责范围。您将创建单独的模块,在PlayerManager中维护玩家状态(例如玩家的健康状况),并在InventoryManager中维护库存列表。这些数据管理器的行为类似于MVC 中的模型;控制器在大多数场景中是不可见的对象(这里不需要,但回想一下前面章节中的SceneController),场景的其余部分类似于视图。
The general idea here is to split up all the data management into separate, well-defined modules, with each managing its own area of responsibility. You’re going to create separate modules to maintain player state in PlayerManager (things like the player’s health) and maintain the inventory list in InventoryManager. These data managers will behave like the model in MVC; the controller is an invisible object in most scenes (it wasn’t needed here, but recall SceneController in previous chapters), and the rest of the scene is analogous to the view.
更高级别的经理将跟踪所有单独的模块。除了保存所有管理器的列表之外,这个更高级别的管理器还将控制各个管理器的生命周期 - 特别是在开始时初始化它们。游戏中的所有其他脚本都可以通过主管理器访问这些集中模块。具体来说,其他代码可以使用主管理器中的静态属性来连接所需的特定模块。
A higher-level manager of managers will keep track of all the separate modules. Besides keeping a list of all the managers, this higher-level manager will control the life cycles of the various managers—in particular, initializing them at the start. All the other scripts in the game will be able to access these centralized modules by going through the main manager. Specifically, other code can use static properties in the main manager to connect with the specific module desired.
为了使主管理器以一致的方式引用其他模块,这些模块必须全部从公共基础继承属性。您将使用接口来实现这一点;许多编程语言(包括 C#)允许您定义其他类需要遵循的一种蓝图。PlayerManager和InventoryManager都将实现一个公共接口(在本例中称为IGameManager),然后主管理器对象可以将PlayerManager和InventoryManager都视为IGameManager类型。图 9.5 说明了我所描述的设置。
For the main manager to reference other modules in a consistent way, these modules must all inherit properties from a common base. You’re going to do that with an interface; many programming languages (including C#) allow you to define a sort of blueprint that other classes need to follow. Both PlayerManager and InventoryManager will implement a common interface (called IGameManager in this case), and then the main Managers object can treat both PlayerManager and InventoryManager as type IGameManager. Figure 9.5 illustrates the setup I’m describing.
Figure 9.5 Diagram of the various modules and how they’re related
顺便说一句,尽管我讨论的所有代码架构都由存在于后台的不可见模块组成,但 Unity 仍然需要将脚本链接到场景中的对象才能运行该代码。正如您在之前的项目中对场景特定的控制器所做的那样,您将创建一个空的 GameObject 来链接这些数据管理器到。
Incidentally, whereas all of the code architecture I’ve been talking about consists of invisible modules that exist in the background, Unity still requires scripts to be linked to objects in the scene in order to run that code. As you’ve done with the scene-specific controllers in previous projects, you’re going to create an empty GameObject to link these data managers to.
全部好的,这解释了您将要执行的操作背后的所有概念;现在是时候编写代码了。首先,创建一个名为IGameManager的新脚本。
All right, so that explains all the concepts behind what you’ll do; it’s time to write the code. To start, create a new script called IGameManager.
Listing 9.9 Base interface that the data managers will implement
公共接口 IGameManager {
ManagerStatus 状态 {get;} ❶
无效启动();
}public interface IGameManager {
ManagerStatus status {get;} ❶
void Startup();
}
❶ This is an enum you need to define.
嗯,这个文件中几乎没有任何代码。请注意,它甚至没有从MonoBehaviour继承;接口本身不做任何事情,存在只是为了将结构强加给其他类。此接口声明一个属性(具有 getter 函数的变量)和一个方法;两者都需要在实现此接口的任何类中实现。status属性告诉其余代码此模块是否已完成初始化。Startup ()的目的是处理管理器的初始化,因此初始化任务发生在那里,并且该函数设置管理器的状态。
Hmm, there’s barely any code in this file. Note that it doesn’t even inherit from MonoBehaviour; an interface doesn’t do anything on its own and exists only to impose structure on other classes. This interface declares one property (a variable that has a getter function) and one method; both need to be implemented in any class that implements this interface. The status property tells the rest of the code whether this module has completed its initialization. The purpose of Startup() is to handle the initialization of the manager, so initialization tasks happen there and the function sets the manager’s status.
请注意,该属性的类型为ManagerStatus。这是您尚未编写的枚举,因此请创建ManagerStatus脚本。
Notice that the property is of type ManagerStatus. That’s an enum you haven’t written yet, so create the ManagerStatus script.
清单 9.10 ManagerStatus : IGameManager状态的可能状态
Listing 9.10 ManagerStatus: possible states for IGameManager status
公共枚举 ManagerStatus {
关闭,
初始化,
已开始
}public enum ManagerStatus {
Shutdown,
Initializing,
Started
}
这是另一个几乎没有任何代码的文件。这一次,您列出了经理可能处于的状态,从而强制要求status属性始终是这些列出的值之一。
This is another file with barely any code in it. This time, you’re listing the possible states that managers can be in, thereby enforcing that the status property will always be one of these listed values.
现在IGameManager已经编写完毕,您可以在其他脚本中实现它。清单 9.11 和 9.12 分别包含InventoryManager和PlayerManager的代码。
Now that IGameManager is written, you can implement it in other scripts. Listings 9.11 and 9.12 contain code for InventoryManager and PlayerManager, respectively.
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 InventoryManager : MonoBehaviour,IGameManager {
公共 ManagerStatus 状态 {获取;私人设置;} ❶
公共无效启动(){
Debug.Log("库存管理器正在启动..."); ❷
status = ManagerStatus.Started; ❸
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class InventoryManager : MonoBehaviour, IGameManager {
public ManagerStatus status {get; private set;} ❶
public void Startup() {
Debug.Log("Inventory manager starting..."); ❷
status = ManagerStatus.Started; ❸
}
}
❶ Property can be read from anywhere but set only within this script.
❷ Any long-running startup tasks go here.
❸对于长时间运行的任务,请改用状态“Initializing”。
❸ For long-running tasks, use status Initializing instead.
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 PlayerManager : MonoBehaviour,IGameManager { ❶
公共 ManagerStatus 状态 {获取;私人设置;}
公共 int 健康 {获取;私人设置;}
公共 int maxHealth {获取;私人设置;}
公共无效启动(){
Debug.Log("玩家管理器正在启动...");
健康 = 50; ❷
最大健康 = 100; ❷
状态 = ManagerStatus.已启动;
}
公共无效ChangeHealth(int值){ ❸
健康+=价值;
如果 (健康 > 最大健康值) {
健康=最大健康值;
} 否则,如果 (健康 < 0) {
健康=0;
}
Debug.Log($"健康:{health}/{maxHealth}");
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerManager : MonoBehaviour, IGameManager { ❶
public ManagerStatus status {get; private set;}
public int health {get; private set;}
public int maxHealth {get; private set;}
public void Startup() {
Debug.Log("Player manager starting...");
health = 50; ❷
maxHealth = 100; ❷
status = ManagerStatus.Started;
}
public void ChangeHealth(int value) { ❸
health += value;
if (health > maxHealth) {
health = maxHealth;
} else if (health < 0) {
health = 0;
}
Debug.Log($"Health: {health}/{maxHealth}");
}
}
❶ Both inherit a class and implement an interface.
❷ These values could be initialized with saved data.
❸ Other scripts can’t set health directly but can call this function.
目前,InventoryManager是一个空壳,稍后会填充,而PlayerManager则具有此项目所需的所有功能。这些管理器都继承自MonoBehaviour类,并实现IGameManager接口。这意味着管理器获得了MonoBehaviour的所有功能,同时还需要实现IGameManager强加的结构。IGameManager 中的结构是一个属性和一个方法,因此管理器定义这两个东西。
For now, InventoryManager is a shell that will be filled in later, whereas PlayerManager has all the functionality needed for this project. These managers both inherit from the MonoBehaviour class and implement the IGameManager interface. That means the managers gain all the functionality of MonoBehaviour while also needing to implement the structure imposed by IGameManager. The structure in IGameManager was one property and one method, so the managers define those two things.
状态属性的定义使得状态可以从任何地方读取(获取器是公共的),但只能在此脚本内设置(设置器是私有的)。接口中的方法是Startup (),因此两个管理器都定义了该函数。在这两个管理器中,初始化都会立即完成(InventoryManager尚未执行任何操作,而PlayerManager设置了几个值),因此状态设置为Started。但数据模块在初始化过程中可能会有长时间运行的任务(例如加载已保存的数据),在这种情况下Startup()将启动这些任务并将管理器的状态设置为Initializing。在这些任务完成后,将状态更改为Started 。
The status property was defined so that the status could be read from anywhere (the getter is public) but set only within this script (the setter is private). The method in the interface is Startup(), so both managers define that function. In both managers, initialization completes right away (InventoryManager doesn’t do anything yet, whereas PlayerManager sets a couple of values), so the status is set to Started. But data modules may have long-running tasks as part of their initialization (such as loading saved data), in which case Startup() will launch those tasks and set the manager’s status to Initializing. Change status to Started after those tasks complete.
太棒了!我们终于准备好将所有内容与经理的主要经理联系起来了。再创建一个脚本并将其命名为Managers。
Great! We’re finally ready to tie everything together with a main manager of managers. Create one more script and call it Managers.
Listing 9.13 The manager of managers!
使用System.Collections; 使用 System.Collections.Generic; 使用 UnityEngine; [RequireComponent(typeof(PlayerManager))] ❶ [RequireComponent(typeof(InventoryManager))] 公共类管理器:MonoBehaviour { 公共静态PlayerManager Player {获取; 私有设置;} ❷ 公共静态 InventoryManager 库存 {获取;私人设置;} 私有 List<IGameManager> startSequence; ❸ 无效唤醒(){ 玩家 = GetComponent<PlayerManager>(); 库存 = GetComponent <InventoryManager> (); 开始序列 = 新列表 <IGameManager>(); 开始序列.添加(玩家); 开始序列.添加(库存); 启动协同程序(StartupManagers()); ❹ } 私有 IEnumerator StartupManagers() { foreach(startSequence 中的 IGameManager 管理器){ 管理器.启动(); } 产量返回 null; int numModules = 启动序列.计数; int 数量就绪 = 0; while (numReady < numModules) { ❺ int lastReady = numReady; 准备数量=0; foreach(startSequence 中的 IGameManager 管理器){ 如果 (manager.status == ManagerStatus.Started) { 数量就绪++; } } 如果 (numReady > lastReady) Debug.Log($"进度:{numReady}/{numModules}"); 产量回报 null; ❻ } Debug.Log("所有管理器已启动"); } }
using System.Collections; using System.Collections.Generic; using UnityEngine; [RequireComponent(typeof(PlayerManager))] ❶ [RequireComponent(typeof(InventoryManager))] public class Managers : MonoBehaviour { public static PlayerManager Player {get; private set;} ❷ public static InventoryManager Inventory {get; private set;} private List<IGameManager> startSequence; ❸ void Awake() { Player = GetComponent<PlayerManager>(); Inventory = GetComponent<InventoryManager>(); startSequence = new List<IGameManager>(); startSequence.Add(Player); startSequence.Add(Inventory); StartCoroutine(StartupManagers()); ❹ } private IEnumerator StartupManagers() { foreach (IGameManager manager in startSequence) { manager.Startup(); } yield return null; int numModules = startSequence.Count; int numReady = 0; while (numReady < numModules) { ❺ int lastReady = numReady; numReady = 0; foreach (IGameManager manager in startSequence) { if (manager.status == ManagerStatus.Started) { numReady++; } } if (numReady > lastReady) Debug.Log($"Progress: {numReady}/{numModules}"); yield return null; ❻ } Debug.Log("All managers started up"); } }
❶ Ensure that the various managers exist.
❷ Static properties that other code uses to access managers
❸ The list of managers to loop through during the startup sequence
❹ Launch the startup sequence asynchronously.
❺ Keep looping until all managers are started.
❻ Pause for one frame before checking again.
此模式最重要的部分是顶部的静态属性。这些属性使其他脚本能够使用Managers.Player或Managers.Inventory等语法来访问各种模块。这些属性最初是空的,但当代码在Awake()方法中运行时,它们会立即填充。
The most important parts of this pattern are the static properties at the top. Those enable other scripts to use syntax like Managers.Player or Managers.Inventory to access the various modules. Those properties are initially empty, but they’re filled immediately when the code runs in the Awake() method.
提示与Start()和Update()一样,Awake是MonoBehaviour自动提供的另一种方法。它类似于Start(),在代码首次开始运行时运行一次。但在 Unity 的代码执行序列中,Awake()比Start()运行得更快,允许在任何其他代码模块之前绝对必须运行的初始化任务。
TIP Like Start() and Update(), Awake is another method automatically provided by MonoBehaviour. It’s similar to Start(), running once when the code first starts running. But in Unity’s code-execution sequence, Awake() runs even sooner than Start(), allowing for initialization tasks that absolutely must run before any other code modules.
Awake()方法还列出了启动顺序,然后启动协程来启动所有管理器。具体来说,该函数创建一个List,然后使用List.Add()添加管理器。
The Awake() method also lists the startup sequence, and then launches the coroutine to start all the managers. Specifically, the function creates a List and then uses List.Add() to add the managers.
定义 List是 C# 提供的集合数据结构。List 对象类似于数组:它们以特定类型声明并按顺序存储一系列条目。但列表在创建后可以更改大小,而数组则以静态大小创建,以后无法更改。
DEFINITION List is a collection data structure provided by C#. List objects are similar to arrays: they’re declared with a specific type and store a series of entries in sequence. But a list can change size after being created, whereas arrays are created at a static size that can’t change later.
因为所有管理器都实现了IGameManager,所以此代码可以将它们全部列为该类型,并可以调用Startup()方法在每个程序中都定义。启动序列作为协同程序运行,因此它将异步运行,游戏的其他部分也将同步进行(例如,启动屏幕上的动画进度条)。
Because all the managers implement IGameManager, this code can list them all as that type and can call the Startup() method defined in each. The startup sequence is run as a coroutine so that it will run asynchronously, with other parts of the game proceeding too (for example, a progress bar animated on a startup screen).
启动函数首先循环遍历整个管理器列表,并对每个管理器调用Startup()。然后它进入一个循环,不断检查管理器是否已启动,直到所有管理器都启动后才会继续。一旦所有管理器都启动,启动函数最终会在最终完成之前提醒我们这一事实。
The startup function first loops through the entire list of managers and calls Startup() on each one. Then it enters a loop that keeps checking whether the managers have started up and won’t proceed until they all have. Once all the managers are started, the startup function finally alerts us to this fact before finally completing.
提示您之前编写的管理器具有非常简单的初始化,无需等待,但通常,这种基于协程的启动序列可以优雅地处理长时间运行的异步启动任务,例如加载已保存的数据。
TIP The managers you wrote earlier have such simple initialization that no waiting is required, but in general this coroutine-based startup sequence can elegantly handle long-running asynchronous startup tasks like loading saved data.
现在所有的代码结构都写好了。回到 Unity 并创建一个新的空 GameObject;像往常一样,对于这些空代码对象,将其定位在0,0,0处,并为该对象指定一个描述性名称,例如Game Managers。将Managers附加到、球员经理和InventoryManager脚本组件添加到这个新对象。
Now all of the code structure has been written. Go back to Unity and create a new empty GameObject; as usual with these sorts of empty code objects, position it at 0, 0, 0 and give the object a descriptive name like Game Managers. Attach the Managers, PlayerManager, and InventoryManager script components to this new object.
现在玩游戏时,场景中不会发生任何明显的变化,但在控制台中,您应该看到一系列消息记录启动序列的进度。假设管理器正确启动,现在是时候开始对库存进行编程了经理。
When you play the game now, no visible change in the scene should occur, but in the console, you should see a series of messages logging the progress of the startup sequence. Assuming the managers are starting up correctly, it’s time to start programming the inventory manager.
这收集的物品列表也可以存储为List对象。此列表将物品列表添加到InventoryManager中。
The list of items collected could also be stored as a List object. This listing adds a list of items to InventoryManager.
清单 9.14 将项目添加到InventoryManager
Listing 9.14 Adding items to InventoryManager
...
私人列表<string>项目;
公共无效启动(){
Debug.Log("库存管理器正在启动...");
items = new List<string>(); ❶
状态 = ManagerStatus.已启动;
}
私有 void DisplayItems() { ❷
字符串 itemDisplay = "项目:";
foreach(项目中的字符串项目){
itemDisplay += item + “ ”;
}
调试.日志(itemDisplay);
}
public void AddItem(string name) { ❸
项目.添加(名称);
显示项目();
}
......
private List<string> items;
public void Startup() {
Debug.Log("Inventory manager starting...");
items = new List<string>(); ❶
status = ManagerStatus.Started;
}
private void DisplayItems() { ❷
string itemDisplay = "Items: ";
foreach (string item in items) {
itemDisplay += item + " ";
}
Debug.Log(itemDisplay);
}
public void AddItem(string name) { ❸
items.Add(name);
DisplayItems();
}
...
❶ Initialize the empty item list.
❷ Print console message of the current inventory.
❸ Other scripts can’t manipulate the item list directly but can call this.
此清单对InventoryManager进行了两项关键添加:一个用于存储项目的List对象和一个可供其他代码调用的公共方法AddItem() 。此函数将项目添加到列表中,然后将列表打印到控制台。现在让我们对CollectibleItem脚本进行一些细微调整调用新的AddItem()方法。
This listing makes two key additions to InventoryManager: a List object to store items in and a public method, AddItem(), that other code can call. This function adds the item to the list and then prints the list to the console. Now let’s make a slight adjustment in the CollectibleItem script to call the new AddItem() method.
清单 9.15在CollectibleItem中使用新的InventoryManager
Listing 9.15 Using the new InventoryManager in CollectibleItem
...
void OnTriggerEnter(Collider 其他){
经理.库存.添加物品(物品名称);
销毁(this.gameObject);
}
......
void OnTriggerEnter(Collider other) {
Managers.Inventory.AddItem(itemName);
Destroy(this.gameObject);
}
...
现在,当您四处收集物品时,您应该会在控制台消息中看到库存在增加。这非常酷,但它确实暴露了List数据结构的一个限制:当您收集多个相同类型的物品(例如收集第二个健康物品)时,您会看到列出的两个副本,而不是聚合所有相同类型的物品(参见图 9.6)。根据您的游戏,您可能希望库存分别跟踪每个物品,但在大多数游戏中,库存应该聚合同一物品的多个副本。可以使用List来实现这一点,但使用Dictionary更自然、更高效。
Now when you run around collecting items, you should see your inventory growing in the console messages. This is pretty cool, but it does expose one limitation of List data structures: as you collect multiples of the same type of item (such as collecting a second Health item), you’ll see both copies listed, instead of aggregating all items of the same type (refer to figure 9.6). Depending on your game, you may want the inventory to track each item separately, but in most games, the inventory should aggregate multiple copies of the same item. It’s possible to accomplish this using List, but it’s done more naturally and efficiently using Dictionary instead.
Figure 9.6 Console message with multiples of the same item listed multiple times
DEFINITION 字典是 C# 提供的另一种集合数据结构。字典中的条目通过标识符(或键)而不是通过其在列表中的位置来访问。这类似于哈希表,但更灵活,因为键实际上可以是任何类型(例如,“返回此GameObject的条目”)。
DEFINITION Dictionary is another collection data structure provided by C#. Entries in the dictionary are accessed by an identifier (or key) rather than by their position in the list. This is similar to a hash table but more flexible, because the keys can be literally any type (for example, “Return the entry for this GameObject”).
将InventoryManager中的代码改为使用Dictionary而不是List。将清单 9.14 中的所有内容替换为清单 9.14 中的代码。
Change the code in InventoryManager to use Dictionary instead of List. Replace everything from listing 9.14 with the code from this listing.
清单 9.16 InventoryManager中的项目字典
Listing 9.16 Dictionary of items in InventoryManager
... 私有字典<string, int>项; ❶ 公共无效启动(){ Debug.Log("库存管理器正在启动..."); 项目 = 新词典 <string,int>(); 状态 = ManagerStatus.已启动; } 私有无效DisplayItems(){ 字符串 itemDisplay = "项目:"; foreach (KeyValuePair<string, int> 项目中的项目) { itemDisplay += item.Key + "(" + item.Value + ") "; } 调试.日志(itemDisplay); } 公共无效AddItem(字符串名称){ 如果 (items.ContainsKey(name)) { ❷ 项目[名称] += 1; } 别的 { 项目[名称] = 1; } 显示项目(); } ...
... private Dictionary<string, int> items; ❶ public void Startup() { Debug.Log("Inventory manager starting..."); items = new Dictionary<string, int>(); status = ManagerStatus.Started; } private void DisplayItems() { string itemDisplay = "Items: "; foreach (KeyValuePair<string, int> item in items) { itemDisplay += item.Key + "(" + item.Value + ") "; } Debug.Log(itemDisplay); } public void AddItem(string name) { if (items.ContainsKey(name)) { ❷ items[name] += 1; } else { items[name] = 1; } DisplayItems(); } ...
❶ Dictionary is declared with two types: the key and the value.
❷ Check for existing entries before entering new data.
总体而言,此代码与之前的代码相同,但存在一些微妙的差异。如果您还不熟悉Dictionary数据结构,请注意,此代码声明了两种类型。List仅声明了一种类型(将要列出的值的类型),而Dictionary则声明了键的类型(即标识符的类型)和值的类型。
Overall, this code looks the same as before, but a few tricky differences exist. If you aren’t already familiar with Dictionary data structures, note that this one was declared with two types. Whereas List was declared with only one type (the type of values that’ll be listed), a Dictionary declares both the type of key (that is, what the identifiers will be) and the type of value.
AddItem()方法中存在更多逻辑。之前,每个项目都会附加到List中,但现在您需要检查Dictionary是否已包含该项目;这就是ContainsKey()方法的作用是。如果是新条目,则计数从 1 开始,但如果条目已经存在,则增加存储的值。使用新代码,您将看到库存消息包含每个项目的汇总计数(参见图 9.7)。
A bit more logic exists in the AddItem() method. Before, every item was appended to the List, but now you need to check whether the Dictionary already contains that item; that’s what the ContainsKey() method is for. If it’s a new entry, then you’ll start the count at 1, but if the entry already exists, then increment the stored value. Play with the new code and you’ll see that the inventory messages have an aggregated count of each item (refer to figure 9.7).
Figure 9.7 Console message with multiples of the same item aggregated
呼,终于,收集的物品可以在玩家的库存中进行管理了!这似乎需要很多代码来处理一个相对简单的问题,如果这就是全部目的,那么,是的,它就太复杂了。不过,这种复杂的代码架构的目的是将所有数据保存在单独的灵活模块中,当游戏变得更加复杂时,这是一种有用的模式。例如,现在您可以编写 UI 显示,代码的单独部分将变得容易得多到处理。
Whew, finally, collected items are managed in the player’s inventory! This probably seems like a lot of code to handle a relatively simple problem, and if this were the entire purpose, then, yeah, it would be over-engineered. The point of this elaborate code architecture, though, is to keep all the data in separate flexible modules, a useful pattern when the game gets more complex. For example, now you can write UI displays, and the separate parts of the code will be much easier to handle.
这库存中的物品集合可以在游戏中以多种方式使用,但所有这些用途首先都依赖于某种库存 UI,以便玩家可以看到他们收集的物品。然后,当库存显示给玩家时,您可以通过允许玩家点击他们的物品来将交互性编程到 UI 中。同样,您将编写几个具体的示例(装备钥匙和消耗健康包),然后您应该能够调整此代码以与其他类型的物品一起使用。
The collection of items in your inventory can be used in multiple ways within the game, but all of those uses first rely on some sort of inventory UI so that players can see their collected items. Then, when the inventory is being shown to the player, you can program interactivity into the UI by enabling players to click their items. Again, you’ll program a couple of specific examples (equipping a key and consuming health packs), and then you should be able to adapt this code to work with other types of items.
注意如第 7 章所述,Unity 既有较旧的即时模式 GUI,也有较新的基于精灵的 UI 系统。我们将在本章中使用即时模式 GUI,因为该系统实现速度更快,需要的设置更少;较少的设置非常适合练习。然而,基于精灵的 UI 系统更加精致,对于实际游戏,您需要更精致的界面。
NOTE As mentioned in chapter 7, Unity has both an older immediate mode GUI and a newer sprite-based UI system. We’ll use the immediate mode GUI in this chapter because that system is faster to implement and requires less setup; less setup is great for practice exercises. The sprite-based UI system is more polished, though, and for an actual game, you’d want a more polished interface.
到要在 UI 中显示项目,首先需要向InventoryManager添加几个方法。目前,项目列表是私有的,只能在管理器中访问。要显示列表,该信息必须具有用于访问数据的公共方法。向InventoryManager添加以下清单中显示的两个方法。
To show the items in a UI display, you first need to add a couple more methods to InventoryManager. Right now, the item list is private and accessible only within the manager. To display the list, that information must have public methods for accessing the data. Add two methods shown in the following listing to InventoryManager.
清单 9.17 向InventoryManager添加数据访问方法
Listing 9.17 Adding data access methods to InventoryManager
...
公共列表<string> GetItemList() { ❶
列表 <string> 列表 = 新列表 <string> (items.Keys);
返回列表;
}
public int GetItemCount(string name) { ❷
如果 (items.ContainsKey(name)) {
返回项目[名称];
}
返回0;
}
......
public List<string> GetItemList() { ❶
List<string> list = new List<string>(items.Keys);
return list;
}
public int GetItemCount(string name) { ❷
if (items.ContainsKey(name)) {
return items[name];
}
return 0;
}
...
❶ Returns a List of all the Dictionary keys
❷ Returns how many of that item are in inventory
GetItemList ()方法返回库存中的物品列表。您可能会想,“等一下,我们不是花了很大的力气才将库存从 List 中转换出来吗? ”现在的区别是每种类型的物品只会在列表中出现一次。例如,如果库存包含两个健康包,那么单词health仍然只会在列表中出现一次。这是因为List是根据Dictionary中的键创建的,而不是根据每个单独的物品创建的。
The GetItemList() method returns a list of items in the inventory. You might be thinking, “Wait a minute, didn’t we just spend lots of effort to convert the inventory away from a List?” The difference now is that each type of item will appear only once in the list. If the inventory contains two health packs, for example, the word health will still appear only once in the list. That’s because the List was created from the keys in the Dictionary, not from every individual item.
GetItemCount ()方法返回库存中给定物品的数量。例如,调用GetItemCount("health")来询问“库存中有多少个健康包?”这样,UI 可以在显示每个物品的同时显示每个物品的数量。
The GetItemCount() method returns a count of how many of a given item are in the inventory. For example, call GetItemCount("health") to ask, “How many health packs are in the inventory?” This way, the UI can display a number of each item along with displaying each item.
将这些方法添加到InventoryManager后,您就可以创建 UI 显示。让我们将所有项目显示在屏幕顶部的水平行中。项目将使用图标显示,因此您需要将这些图像导入到项目中。如果这些资产位于名为 Resources 的文件夹中,Unity 将以特殊方式处理这些资产。
With these methods added to InventoryManager, you can create the UI display. Let’s display all the items in a horizontal row across the top of the screen. The items will be displayed using icons, so you need to import those images into the project. Unity handles assets in a special way if those assets are in a folder called Resources.
提示:放置在 Resources 文件夹中的资源可以通过使用Resources.Load()方法在代码中加载。否则,只能通过 Unity 的编辑器将资产放置在场景中。
TIP Assets placed into the Resources folder can be loaded in code by using the Resources.Load() method. Otherwise, assets can be placed in scenes only through Unity’s editor.
图 9.8 显示了四个图标图像,以及显示放置这些图像的位置的目录结构。创建一个名为Resources的文件夹,然后在其中创建一个名为Icons的文件夹。
Figure 9.8 shows the four icon images, along with the directory structure showing where to put those images. Create a folder called Resources and then create a folder called Icons inside it.
图 9.8 放置在 Resources 文件夹中的设备图标图像资源
Figure 9.8 Image assets for equipment icons placed inside the Resources folder
图标都已设置好,因此创建一个名为Controller 的新空 GameObject ,然后为其分配一个名为BasicUI的新脚本。
The icons are all set up, so create a new empty GameObject named Controller and then assign it a new script called BasicUI.
Listing 9.18 BasicUI to display the inventory
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类BasicUI:MonoBehaviour {
无效的OnGUI(){
int posX = 10;
int 位置Y = 10;
int宽度=100;
int 高度 = 30;
int缓冲区=10;
列表<string> itemList = Managers.Inventory.GetItemList();
如果 (itemList.Count == 0) { ❶
GUI.Box(new Rect(posX, posY, width, height), "无项目");
}
foreach(itemList 中的字符串项){
int 计数 = 经理.Inventory.GetItemCount(项目);
Texture2D 图像 = Resources.Load<Texture2D>($"Icons/{item}"); ❷
GUI.Box(新Rect(posX,posY,宽度,高度),
新的 GUIContent($"({count})", 图像));
posX += 宽度 + 缓冲区; ❸
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BasicUI : MonoBehaviour {
void OnGUI() {
int posX = 10;
int posY = 10;
int width = 100;
int height = 30;
int buffer = 10;
List<string> itemList = Managers.Inventory.GetItemList();
if (itemList.Count == 0) { ❶
GUI.Box(new Rect(posX, posY, width, height), "No Items");
}
foreach (string item in itemList) {
int count = Managers.Inventory.GetItemCount(item);
Texture2D image = Resources.Load<Texture2D>($"Icons/{item}"); ❷
GUI.Box(new Rect(posX, posY, width, height),
new GUIContent($"({count})", image));
posX += width+buffer; ❸
}
}
}
❶ Display a message if the inventory is empty.
❷ Method loads assets from the Resources folder.
❸ Shift sideways each time through the loop.
此列表在水平行中显示收集的项目(见图 9.9),并显示收集的数量。如第 3 章所述,每个MonoBehaviour都会自动响应OnGUI()方法。该函数在 3D 场景渲染后每一帧都运行。
This listing displays the collected items in a horizontal row (see figure 9.9) along with displaying the number collected. As mentioned in chapter 3, every MonoBehaviour automatically responds to an OnGUI() method. That function runs every frame right after the 3D scene is rendered.
Figure 9.9 UI display of the inventory
在OnGUI()中,首先定义一组用于定位 UI 元素的值。当您循环遍历所有项目以将 UI 元素定位在一行中时,这些值会递增。绘制的特定 UI 元素是GUI.Box;它们是非交互式显示,在框内显示文本和图像。
Inside OnGUI(), first define a bunch of values for positioning UI elements. These values are incremented when you loop through all the items in order to position UI elements in a row. The specific UI element drawn is GUI.Box; those are noninteractive displays that show text and images inside boxes.
Resources.Load ()方法用于从 Resources 文件夹加载资源。此方法是按名称加载资源的便捷方法;请注意,项目的名称作为参数传递。您必须指定要加载的类型。否则,该方法的返回值是通用对象。
The Resources.Load() method is used to load assets from the Resources folder. This method is a handy way to load assets by name; notice that the name of the item is passed as a parameter. You have to specify a type to load. Otherwise, the return value for that method is a generic object.
The UI shows us what items have been collected. Now you can use the items.
让我们介绍几个使用库存物品的例子,以便您可以推断出您想要的任何类型的物品。第一个例子涉及配备开门所需的钥匙。
Let’s go over a couple of examples of using inventory items so that you can extrapolate out to any type of item you want. The first example involves equipping a key required to open the door.
目前,DeviceTrigger脚本不会关注您的物品(因为该脚本是在库存代码之前编写的)。此清单显示了如何调整该脚本。
At the moment, the DeviceTrigger script doesn’t pay attention to your items (because that script was written before the inventory code). This listing shows how to adjust that script.
Listing 9.19 Requiring a key in DeviceTrigger
...
公共 bool requireKey;
void OnTriggerEnter(Collider 其他){
如果 (requireKey && Managers.Inventory.equippedItem != "key") {
返回;
}
......
public bool requireKey;
void OnTriggerEnter(Collider other) {
if (requireKey && Managers.Inventory.equippedItem != "key") {
return;
}
...
如您所见,所需的只是脚本中的一个新的公共变量和一个查找已配备密钥的条件。requireKey布尔值在检查器中显示为复选框,这样您就可以要求某些触发器中的键,而不需要其他触发器中的键。OnTriggerEnter ()开头的条件检查InventoryManager中是否配备了键;这要求您将下一个列表中的代码添加到InventoryManager中。
As you can see, all that’s needed is a new public variable in the script and a condition that looks for an equipped key. The requireKey Boolean appears as a check box in the Inspector so that you can require a key from some triggers but not others. The condition at the beginning of OnTriggerEnter() checks for an equipped key in InventoryManager; that requires you to add the code from the next listing to InventoryManager.
Listing 9.20 Equipping code for InventoryManager
...
公共字符串 equippedItem {获取;私人设置;}
...
公共 bool EquipItem(字符串名称){
如果 (items.ContainsKey(name) && equippedItem != name) { ❶
装备物品 = 名称;
Debug.Log($"已装备{name}");
返回 true;
}
装备物品 = 空;
Debug.Log("未装备");
返回 false;
}
......
public string equippedItem {get; private set;}
...
public bool EquipItem(string name) {
if (items.ContainsKey(name) && equippedItem != name) { ❶
equippedItem = name;
Debug.Log($"Equipped {name}");
return true;
}
equippedItem = null;
Debug.Log("Unequipped");
return false;
}
...
❶ Check that inventory has the item and that the item isn’t already equipped.
在顶部,添加equippedItem属性由其他代码检查。然后添加公共EquipItem()方法允许其他代码更改装备的物品。该方法如果物品尚未装备则装备该物品,如果物品已装备则取消装备。
At the top, add the equippedItem property that gets checked by other code. Then add the public EquipItem() method to allow other code to change which item is equipped. That method equips an item if it isn’t already equipped, or unequips if that item is already equipped.
最后,为了让玩家装备物品,请将该功能添加到 UI。此列表为此目的添加了一行按钮。
Finally, in order for the player to equip an item, add that functionality to the UI. This listing adds a row of buttons for that purpose.
Listing 9.21 Adding equip functionality to BasicUI
...
foreach (itemList 中的字符串项) { ❶
...
posX += 宽度 + 缓冲区;
}
字符串配备 = Managers.Inventory.equippedItem;
如果 (equipped != null) { ❷
posX = 屏幕.宽度 - (宽度+缓冲区);
Texture2D 图像 = Resources.Load($"Icons/{equipped}") 作为 Texture2D;
GUI.Box(新Rect(posX,posY,宽度,高度),
新的 GUIContent("已装备", 图像));
}
位置X = 10;
posY += 高度+缓冲区;
foreach (itemList 中的字符串项) { ❸
如果(GUI.Button(new Rect(posX,posY,宽度,高度),
$"装备 {item}")) { ❹
经理.库存.装备物品(物品);
}
posX += 宽度 + 缓冲区;
}
}
}...
foreach (string item in itemList) { ❶
...
posX += width+buffer;
}
string equipped = Managers.Inventory.equippedItem;
if (equipped != null) { ❷
posX = Screen.width - (width+buffer);
Texture2D image = Resources.Load($"Icons/{equipped}") as Texture2D;
GUI.Box(new Rect(posX, posY, width, height),
new GUIContent("Equipped", image));
}
posX = 10;
posY += height+buffer;
foreach (string item in itemList) { ❸
if (GUI.Button(new Rect(posX, posY, width, height),
$"Equip {item}")) { ❹
Managers.Inventory.EquipItem(item);
}
posX += width+buffer;
}
}
}
❶ Italicized code was already in the script, shown here for reference.
❷ Display the currently equipped item.
❸ Loop through all items to make buttons.
❹ Run the contained code if the button is clicked.
再次使用GUI.Box()来显示装备好的物品。但该元素是非交互式的,因此使用GUI.Button()来绘制装备按钮行。该方法创建一个按钮,执行if中的代码语句当被点击时。
GUI.Box() is used again to display the equipped item. But that element is noninteractive, so the row of Equip buttons is drawn using GUI.Button() instead. That method creates a button that executes the code inside the if statement when clicked.
准备好所有必要的代码后,选择requireKey选项在DeviceTrigger中,然后玩游戏。尝试在装备钥匙之前跑进触发体积;什么都没有发生。现在收集一把钥匙并单击按钮来装备它。跑进触发体积会打开门。
With all the necessary code in place, select the requireKey option in DeviceTrigger and then play the game. Try running into the trigger volume before equipping a key; nothing happens. Now collect a key and click the button to equip it. Running into the trigger volume opens the door.
只是为了好玩,你可以把钥匙放在位置-11、5、-14处,以增加一个简单的游戏挑战,看看你是否能弄清楚如何到达钥匙。无论你是否尝试,让我们继续使用健康包。
Just for fun, you could put a key at Position -11, 5, -14 to add a simple gameplay challenge to see if you can figure out how to reach the key. Whether or not you try that, let’s move on to using health packs.
使用另一个普遍适用的例子是使用物品来恢复玩家的健康。这需要更改两个代码: InventoryManager中的新方法和 UI 中的新按钮(分别参见清单 9.22 和 9.23)。
Using items to restore the player’s health is another generally useful example. That requires two code changes: a new method in InventoryManager and a new button in the UI (see listings 9.22 and 9.23, respectively).
Listing 9.22 New method in InventoryManager
...
公共 bool ConsumeItem(字符串名称){
如果 (items.ContainsKey(name)) { ❶
项目[名称]--;
if (items[name] == 0) { ❷
项目。删除(名称);
}
}否则{ ❸
Debug.Log($"无法使用{name}");
返回 false;
}
显示项目();
返回 true;
}
......
public bool ConsumeItem(string name) {
if (items.ContainsKey(name)) { ❶
items[name]--;
if (items[name] == 0) { ❷
items.Remove(name);
}
} else { ❸
Debug.Log($"Cannot consume {name}");
return false;
}
DisplayItems();
return true;
}
...
❶ Check whether the item is in inventory.
❷ Remove the entry if the count goes to 0.
❸ Response if that item isn’t in inventory
Listing 9.23 Adding a health item to BasicUI
...
foreach (itemList 中的字符串项) { ❶
if (GUI.Button(new Rect(posX, posY, width, height),
$"装备{item}")) {
经理.库存.装备物品(物品);
}
如果 (item == “健康”) { ❷
如果(GUI.Button(new Rect(posX,posY +高度+缓冲区,宽度,
高度), “使用健康”)){ ❸
经理.Inventory.ConsumeItem("健康");
经理.球员.改变健康(25);
}
}
posX += 宽度 + 缓冲区;
}
}
}...
foreach (string item in itemList) { ❶
if (GUI.Button(new Rect(posX, posY, width, height),
$"Equip {item}")) {
Managers.Inventory.EquipItem(item);
}
if (item == "health") { ❷
if (GUI.Button(new Rect(posX, posY + height+buffer, width,
height), "Use Health")) { ❸
Managers.Inventory.ConsumeItem("health");
Managers.Player.ChangeHealth(25);
}
}
posX += width+buffer;
}
}
}
❶ Italicized code was already in script, shown here for reference.
❸ Run the contained code if the button is clicked.
新的ConsumeItem()方法与AddItem()基本相反。它会检查库存中的物品,如果找到该物品,则减少该物品。它对几个棘手的情况有响应,例如,如果物品数量减少到 0。UI 代码调用这个新的库存方法,并调用 ChangeHealth ()方法PlayerManager从一开始就拥有这个权限。
The new ConsumeItem() method is pretty much the reverse of AddItem(). It checks for an item in the inventory and decrements if the item is found. It has responses to a couple of tricky cases, such as if the item count decrements to 0. The UI code calls this new inventory method, and it calls the ChangeHealth() method that PlayerManager has had from the beginning.
如果你收集一些健康物品然后使用它们,你会看到健康信息出现在控制台中。就是这样——多个如何使用的示例存货项目!
If you collect some health items and then use them, you’ll see health messages appear in the console. And there you go—multiple examples of how to use inventory items!
Both keypresses and collision triggers can be used to operate devices.
Objects with physics enabled can respond to collision forces or trigger volumes.
Complex game state is managed via special objects that can be accessed globally.
Collections of objects can be organized in List or Dictionary data structures.
Tracking the equip state of items can be used to affect other parts of the game.
现在,您已经对 Unity 有了相当多的了解。您知道如何编写玩家控件、创建四处游荡的敌人以及向游戏中添加交互式设备。您甚至知道如何使用 2D 和 3D 图形构建游戏!这几乎就是开发完整游戏所需的全部知识,但还不够。您仍然需要了解一些最后的任务,例如将音频放入游戏,并且您需要了解如何将我们一直在处理的所有不同部分组合在一起。这是最后冲刺,只剩下四章了!
You know a fair amount about Unity by now. You know how to program the player’s controls, create enemies that wander around, and add interactive devices to the game. You even know how to build a game using both 2D and 3D graphics! That’s almost everything you need to know to develop a complete game, but not quite. You still need to learn about a few final tasks, like putting audio in the game, and you need to understand how to put together all the disparate pieces we’ve been working with. This is the home stretch, with just four chapters left!
在本章中,您将学习如何通过网络发送和接收数据。前几章中构建的项目代表了各种游戏类型,但都与玩家的机器隔离。对于所有类型的游戏来说,连接互联网和交换数据都越来越重要。
In this chapter, you’ll learn how to send and receive data over a network. The projects built in previous chapters represented a variety of game genres, but all have been isolated to the player’s machine. Connecting to the internet and exchanging data is increasingly important for games in all genres.
许多游戏几乎完全存在于互联网上,与其他玩家社区保持持续联系;这类游戏被称为大型多人在线游戏(MMO)并通过 MMO 角色扮演游戏广为人知(MMORPG)。即使游戏不需要如此持续的连接,现代视频游戏通常也会包含诸如向全球高分列表报告分数之类的功能,或者记录分析以帮助改进游戏。Unity 提供了对此类网络的支持,因此我们将介绍这些功能。
Many games exist almost entirely over the internet, with constant connection to a community of other players; games of this sort are referred to as massively multiplayer online (MMO) and are most widely known through MMO role-playing games (MMORPGs). Even when a game doesn’t require such constant connectivity, modern video games usually incorporate features like reporting scores to a global list of high scores, or they record analytics to help improve the game. Unity provides support for such networking, so we’ll be going over those features.
Unity 支持多种网络通信方法,因为不同的方法更适合不同的需求。本章介绍最常见的互联网通信方式:发出 HTTP 请求。
Unity supports multiple approaches to network communication, since different approaches are better suited to different needs. This chapter covers the most general sort of internet communication: issuing HTTP requests.
作为一个很好的比较,想象一下现代单页 Web 应用程序的工作方式(与基于服务器端生成的网页的老式 Web 开发相反)。在围绕 HTTP 请求构建的在线游戏中,使用 Unity 开发的项目本质上是一个以 Ajax 风格与服务器通信的胖客户端。然而,这种方法的熟悉性可能会误导经验丰富的 Web 开发人员。视频游戏的性能要求通常比 Web 应用程序严格得多,这些差异可能会影响设计决策。
As a good comparison, imagine how a modern single-page web application works (as opposed to old-school web development based on web pages generated server-side). In an online game built around HTTP requests, the project developed in Unity is essentially a thick client that communicates with the server in an Ajax style. However, the familiarity of this approach can be misleading for experienced web developers. Video games often have much more stringent performance requirements than web applications, and these differences can affect design decisions.
警告:网络应用和视频游戏的时间尺度可能大不相同。对于更新网站来说,半秒钟似乎只是很短的等待时间,但在高强度动作游戏中,哪怕只是暂停一小段时间,也会让人非常痛苦。快速的概念肯定是相对于具体情况而言的。
WARNING Time scales can be vastly different between web apps and video games. Half a second can seem like a short wait for updating a website, but pausing even just a fraction of that time can be excruciating in the middle of a high-intensity action game. The concept of fast is definitely relative to the situation.
在线游戏通常会连接到专门为该游戏设计的服务器。但为了学习目的,我们将连接到一些免费的互联网数据源,包括天气数据和我们可以下载的图像。本章的最后一节要求您设置自定义 Web 服务器;由于这一要求,该部分是可选的,尽管我将解释一种使用开源软件轻松完成此操作的方法。
Online games usually connect to a server specifically intended for that game. For learning purposes, however, we’ll connect to some freely available internet data sources, including both weather data and images we can download. The last section of this chapter requires you to set up a custom web server; that section is optional because of that requirement, although I’ll explain an easy way to do it with open source software.
本章的计划是介绍 HTTP 请求的多种用途,以便您了解它们在 Unity 中的工作方式:
The plan for this chapter is to go over multiple uses of HTTP requests so you can learn how they work within Unity:
Setting up an outdoor scene (in particular, building a sky that can react to the weather data)
Parsing the response and then modifying the scene based on the data
Posting data to your own server (in this case, a log of weather conditions)
您将在本章的项目中使用的实际游戏并不重要。本章中的所有内容都会向现有项目添加新脚本,并且不会修改任何现有代码。对于示例代码,我使用了第 2 章中的移动演示,主要是因为我们可以在修改后在第一人称视角中看到天空。
The actual game that you’ll use for this chapter’s project matters little. Everything in this chapter will add new scripts to an existing project and won’t modify any of the existing code. For the sample code, I used the movement demo from chapter 2, mostly so we can see the sky in first-person view when it gets modified.
本章的项目与游戏玩法没有直接关系,但显然对于您创建的大多数游戏,您都希望网络与游戏玩法相关联(例如,根据服务器的响应生成敌人)。迈出第一步!
The project for this chapter isn’t directly tied into the gameplay, but obviously for most games you create, you would want the networking tied to the gameplay (for example, spawning enemies based on responses from the server). On to the first step!
因为我们要下载天气数据,首先要设置一个可以看到天气的户外区域。其中最棘手的部分是天空,但首先让我们花点时间在关卡几何体上应用户外纹理。
Because we’re going to be downloading weather data, we’ll first set up an outdoor area where the weather will be visible. The trickiest part of that will be the sky, but first let’s take a moment to apply outdoors-looking textures on the level geometry.
就像在第 4 章中一样,我从www.textures.com获取了一些图像,用于应用到关卡的墙壁和地板上。请记住将下载的图像的大小更改为 2 的幂,例如 256 × 256。
Just as in chapter 4, I obtained a couple of images from www.textures.com to apply to the walls and floor of the level. Remember to change the size of the downloaded images to a power of 2, such as 256 × 256.
然后将图像导入 Unity 项目,创建材质,并将图像分配给材质(即,将图像拖到材质的纹理槽中)。将材质拖到场景中的墙壁或地板上,并增加材质中的平铺(尝试在一个或两个方向上使用 8 或 9 之类的数字),这样图像就不会以难看的方式被拉伸。处理完地面和墙壁后,就该处理天空了。
Then import the images into the Unity project, create materials, and assign the images to the materials (that is, drag an image into the texture slot of the material). Drag the materials onto the walls or floor in the scene, and increase tiling in the material (try numbers like 8 or 9 in one or both directions) so that the image won’t be stretched in an ugly way. Once the ground and walls are taken care of, it’s time to address the sky.
开始通过导入天空盒图像(如第 4 章中所述)。我再次从www.93i.de/获取天空盒图像,但这次除了 TropicalSunnyDay 之外,我还获得了 DarkStormy 套装(此项目中的天空将更加复杂)。只需从本书的示例项目中获取它们,或下载您在其他地方找到的天空盒图像。将这些纹理导入 Unity 并(如第 4 章中所述)将其 Wrap Mode 设置为 Clamp。
Start by importing the skybox images as you did in chapter 4. Once again, I obtained skybox images from www.93i.de/, but this time I got the DarkStormy set in addition to TropicalSunnyDay (the sky will be more complex in this project). Simply get them from the book’s sample project or download skybox images you find elsewhere. Import these textures into Unity and (as explained in chapter 4) set their Wrap Mode to Clamp.
现在创建一个用于此天空盒的新材质。在此材质的设置顶部,单击“着色器”菜单以查看包含所有可用着色器的下拉列表。向下移动到“天空盒”部分并在该子菜单中选择“6 面”。激活此着色器后,该材质现在有六个纹理槽(而不是标准着色器仅有的小型 Albedo 纹理槽)。
Now create a new material to use for this skybox. At the top of the settings for this material, click the Shader menu to see the drop-down list with all the available shaders. Move down to the Skybox section and choose 6-Sided in that submenu. With this shader active, the material now has six texture slots (instead of only the small Albedo texture slot that the standard shader had).
将 SunnyDay 天空盒图像拖到新材质的纹理槽中。图像的名称与要分配到的纹理槽相对应(顶部、正面等)。一旦所有六个纹理都链接起来,您就可以使用这个新材质作为场景的天空盒。
Drag the SunnyDay skybox images to the texture slots of the new material. The names of the images correspond to the texture slot to assign them to (top, front, and so on). Once all six textures are linked, you can use this new material as the skybox for the scene.
打开 Lighting 窗口(Window > Rendering > Lighting)来指定天空盒材质。切换到 Environment 选项卡,将天空盒材质指定到窗口顶部的 Skybox 插槽(将材质拖过来或单击插槽旁边的小圆圈按钮)。单击 Play,您应该会看到类似图 10.1 的内容。
Assign this skybox material by opening the Lighting window (Window > Rendering > Lighting). Switch to the Environment tab and assign the material for your skybox to the Skybox slot at the top of the window (either drag the material over or click the little circle button next to the slot). Click Play and you should see something like figure 10.1.
Figure 10.1 Scene with background pictures of the sky
太棒了,现在您有了一个户外场景!天空盒是一种优雅的方式,可以营造出玩家周围广阔氛围的幻觉。但是 Unity 内置的天空盒着色器确实有一个重大限制:图像永远不会改变,导致天空看起来完全是静态的。我们将通过创建一个新的自定义天空盒着色器来解决这个限制着色器。
Great, now you have an outdoors scene! A skybox is an elegant way to create the illusion of a vast atmosphere surrounding the player. But the skybox shader built into Unity does have one significant limitation: the images can never change, resulting in a sky that appears completely static. We’ll address that limitation by creating a new custom shader.
这TropicalSunnyDay 集中的图像非常适合晴天,但如果我们想在晴天和阴天之间过渡怎么办?这将需要第二组天空图像(一些多云天空的图片),因此我们需要一个新的天空盒着色器。
The images in the TropicalSunnyDay set look great for a sunny day, but what if we want to transition between sunny and overcast weather? This will require a second set of sky images (some pictures of a cloudy sky), so we need a new shader for the skybox.
如第 4 章所述,着色器是一个简短的程序,其中包含有关如何渲染图像的说明。这意味着您可以编写新的着色器,事实上也确实如此。我们将创建一个新的着色器,它采用两组天空盒图像并在它们之间进行转换。从https://github.com/jhocking/from-unity-wiki/blob/main/SkyboxBlended.shader获取用于此目的的着色器。
As explained in chapter 4, a shader is a short program with instructions for how to render the image. This implies that you can program new shaders, and that is, in fact, the case. We’re going to create a new shader that takes two sets of skybox images and transitions between them. Get a shader for this purpose from https://github.com/jhocking/from-unity-wiki/blob/main/SkyboxBlended.shader.
在 Unity 中,创建一个新的着色器脚本:转到“创建”菜单,就像创建新的 C# 脚本一样,但选择“标准表面着色器”。将资源命名为SkyboxBlended,然后双击着色器以打开脚本。从该网页复制代码并将其粘贴到着色器脚本中。最上面一行是Shader "Skybox/Blended",它告诉 Unity 将新着色器添加到 Skybox 类别(与常规天空盒相同的类别)下的着色器列表中。
In Unity, create a new shader script: Go to the Create menu just like when you create a new C# script, but select a Standard Surface Shader instead. Name the asset SkyboxBlended and then double-click the shader to open the script. Copy the code from that webpage and paste it into the shader script. The top line is Shader "Skybox/Blended", which tells Unity to add the new shader into the shader list under the Skybox category (the same category as the regular skybox).
注意我们现在不会讨论着色器程序的所有细节。着色器编程是一个相当高级的计算机图形学主题,因此超出了本书的范围。读完本书后,您可能想查阅一下;如果是这样,请从http://mng.bz/wQzQ上的 Unity 手册开始。
NOTE We won’t go over all the details of the shader program right now. Shader programming is a pretty advanced computer graphics topic, thus outside the scope of this book. You may want to look that up after you’ve finished this book; if so, start with the Unity Manual at http://mng.bz/wQzQ.
现在您可以将材质设置为 Skybox Blended 着色器。再次选择材质,然后在材质设置顶部查找着色器菜单。现在有 12 个纹理槽,分为两组,每组 6 个图像。像以前一样将 TropicalSunnyDay 图像分配给前六个纹理;对于其余纹理,使用 DarkStormy 天空盒图像集。
Now you can set your material to the Skybox Blended shader. Again, select the material and then look for the Shader menu at the top of the material’s settings. There are now 12 texture slots, in two sets of six images. Assign TropicalSunnyDay images to the first six textures just as before; for the remaining textures, use the DarkStormy set of skybox images.
这个新着色器还在设置顶部附近添加了一个混合滑块。混合值控制您要显示每组天空盒图像的数量;当您将滑块从一侧调整到另一侧时,天空盒会从晴天过渡到阴天。您可以通过调整滑块并玩游戏来进行测试,但在游戏运行时手动调整天空并没有太大帮助,所以让我们编写代码来转换天空。
This new shader also added a Blend slider near the top of the settings. The Blend value controls how much of each set of skybox images you want to display; when you adjust the slider from one side to the other, the skybox transitions from sunny to overcast. You can test by adjusting the slider and playing the game, but manually adjusting the sky isn’t terribly helpful while the game is running, so let’s write code to transition the sky.
在场景中创建一个空对象并将其命名为Controller。创建一个新脚本并将其命名为WeatherController。将该脚本拖到空对象上,然后在该脚本中写入此列表。
Create an empty object in the scene and name it Controller. Create a new script and name it WeatherController. Drag that script onto the empty object and then write this listing in that script.
清单 10.1 WeatherController脚本从晴天过渡到阴天
Listing 10.1 WeatherController script transitioning from sunny to overcast
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类WeatherController:MonoBehaviour {
[SerializeField] 材质天空; ❶
[SerializeField] 光太阳;
私人浮动全强度;
私有浮点云值 = 0f;
无效开始(){
fullIntensity = 太阳.intensity; ❷
}
无效更新(){
设置阴天(云值);
云值 += .005f; ❸
}
私有 void SetOvercast(浮点值) { ❹
天空.SetFloat(“_Blend”,值);
太阳强度 = 全强度 - (全强度 * 值);
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WeatherController : MonoBehaviour {
[SerializeField] Material sky; ❶
[SerializeField] Light sun;
private float fullIntensity;
private float cloudValue = 0f;
void Start() {
fullIntensity = sun.intensity; ❷
}
void Update() {
SetOvercast(cloudValue);
cloudValue += .005f; ❸
}
private void SetOvercast(float value) { ❹
sky.SetFloat("_Blend", value);
sun.intensity = fullIntensity - (fullIntensity * value);
}
}
❶ Reference the material in Project view, not only objects in the scene.
❷ Initial intensity of the light is considered “full” intensity.
❸ Increment the value every frame for a continuous transition.
❹ Adjust both the material’s Blend value and the light’s intensity.
我将指出此代码中的几件事,但关键的新方法是SetFloat(),它几乎出现在底部。到目前为止的所有内容都应该相当熟悉,但这一点是新的。该方法在材质上设置一个数值。该方法的第一个参数定义了具体哪个值。在本例中,材质有一个名为 _ Blend 的属性(请注意,代码中的材质属性以下划线开头)。
I’ll point out several things in this code, but the key new method is SetFloat(), which appears almost at the bottom. Everything up to that point should be fairly familiar, but that one is new. The method sets a number value on the material. The first parameter to that method defines which value specifically. In this case, the material has a property called _Blend (note that material properties in code start with an underscore).
至于代码的其余部分,我们定义了一些变量,包括材质和灯光。对于材质,您想引用我们刚刚创建的混合天空盒材质,但灯光呢?这样当从晴天过渡到阴天时,场景也会变暗;随着混合值的增加,我们会调低灯光。场景中的定向光充当主光,并在任何地方提供照明。将材质和灯光都拖到 Inspector 中的变量上。
As for the rest of the code, we define a few variables, including both the material and a light. For the material, you want to reference the blended skybox material we just created, but what’s with the light? That’s so that the scene will also darken when transitioning from sunny to overcast; as the Blend value increases, we’ll turn down the light. The directional light in the scene acts as the main light and provides illumination everywhere. Drag both the material and the light onto the variables in the Inspector.
注意Unity 中的高级照明系统会考虑天空盒来实现逼真的效果。但是这种照明方法在天空盒变化时无法正常工作,因此您可能需要冻结照明设置。在照明窗口中,您可以关闭底部的自动生成复选框;然后设置只会在您单击按钮时更新。将天空盒的混合设置为中间以获得平均外观,然后单击生成按钮(自动复选框旁边)以手动烘焙光照贴图(照明信息保存在以场景命名的新文件夹中)。
NOTE The advanced lighting system in Unity takes the skybox into account to achieve realistic results. But this lighting approach won’t work right with a changing skybox, so you may want to freeze the lighting setup. In the Lighting window, you can turn off the Auto Generate check box at the bottom; then the setup will update only when you click the button. Set the Blend of the skybox to the middle for an average look and then click the Generate button (next to the Auto check box) to manually bake lightmaps (lighting information was saved in a new folder that’s named after the scene).
脚本启动时,会初始化灯光的强度。脚本会存储起始值,并将其视为最大强度。脚本稍后会在调暗灯光时使用此最大强度。
When the script starts, it initializes the intensity of the light. The script will store the starting value and consider that to be the full intensity. This full intensity will be used later in the script when dimming the light.
然后代码每帧增加一个值,并使用该值调整天空。具体来说,它每帧调用SetOvercast(),该函数封装了对场景所做的多项调整。我已经解释了SetFloat()的作用,所以我们不再赘述,最后一行调整光的强度。
Then the code increments a value every frame and uses that value to adjust the sky. Specifically, it calls SetOvercast() every frame, and that function encapsulates the multiple adjustments made to the scene. I’ve already explained what SetFloat() is doing, so we won’t go over that again, and the last line adjusts the intensity of the light.
现在播放场景以查看代码的运行情况。您将看到图 10.2 中的描述:几秒钟后,您将看到场景从晴天转变为阴天。
Now play the scene to watch the code running. You’ll see the depiction in figure 10.2: over a couple of seconds, you’ll see the scene transition from a sunny day to dark and overcast.
Figure 10.2 Before and after: scene transition from sunny to overcast
警告Unity 的一个意外怪癖是材质的Blend更改是永久性的。当游戏停止运行时,Unity 会重置场景中的对象,但直接从 Project 视图链接的资产(例如天空盒材质)会永久更改。这种情况只发生在 Unity 的编辑器中(在游戏部署到编辑器之外后,更改不会在游戏之间延续),因此如果您忘记了它,就会产生令人沮丧的错误。
WARNING One unexpected quirk about Unity is that the Blend change on the material is permanent. Unity resets objects in the scene when the game stops running, but assets that were linked directly from the Project view (such as the skybox material) are changed permanently. This happens only within Unity’s editor (changes don’t carry over between plays after the game is deployed outside the editor), thus resulting in frustrating bugs if you forget about it.
观看场景从晴天过渡到阴天非常酷。但这只是为实际目标所做的准备:让游戏中的天气与现实世界的天气条件同步。为此,我们需要开始从这互联网。
Watching the scene transition from sunny to overcast is pretty cool. But this was all just a setup for the actual goal: having the weather in the game sync up to real-world weather conditions. For that, we need to start downloading weather data from the internet.
现在既然我们已经设置了户外场景,我们就可以编写代码来下载天气数据并根据该数据修改场景。此任务将提供使用 HTTP 请求检索数据的一个很好的示例。许多 Web 服务都提供天气数据; ProgrammableWeb ( www.programmableweb.com)上发布了详尽的列表。我选择了 OpenWeather;代码示例使用了位于http://openweathermap.org/api的 API(应用程序编程接口,一种使用代码命令而不是图形界面访问其服务的方式)。
Now that we’ve set up the outdoors scene, we can write code that will download weather data and modify the scene based on that data. This task will provide a good example of retrieving data by using HTTP requests. Many web services provide weather data; an extensive list is posted at ProgrammableWeb (www.programmableweb.com). I chose OpenWeather; the code examples use its API (application programming interface, a way to access their service using code commands instead of a graphical interface) located at http://openweathermap.org/api.
定义Web服务或Web API,是连接到互联网并根据请求返回数据的服务器。Web API 和网站之间没有技术差异;网站是一种恰好返回网页数据的 Web 服务,而浏览器将 HTML 数据解释为可见文档。
DEFINITION A web service, or web API, is a server connected to the internet that returns data upon request. There’s no technical difference between a web API and a website; a website is a web service that happens to return the data for a web page, and browsers interpret HTML data as a visible document.
注意: Web 服务通常需要您注册,即使是免费服务也是如此。例如,如果您访问 OpenWeather 的 API 页面,其中会提供获取 API 密钥的说明,该密钥是您将粘贴到请求中的值。
NOTE Web services often require you to register, even for free service. For example, if you go to the API page for OpenWeather, it has instructions for obtaining an API key, a value you will paste into requests.
你编写的代码将围绕第 9 章中的相同管理器架构进行构建。这次,你将有一个WeatherManager类由中央管理器初始化。WeatherManager将负责检索和存储天气数据,但要做到这一点,它需要能够与互联网通信。
The code you’ll write will be structured around the same Managers architecture from chapter 9. This time, you’ll have a WeatherManager class that gets initialized from the central manager of managers. WeatherManager will be in charge of retrieving and storing weather data, but to do so, it’ll need the ability to communicate with the internet.
为了实现这一点,您将创建一个名为NetworkService 的实用程序类来处理连接到互联网和发出 HTTP 请求的细节。然后, WeatherManager可以告诉NetworkService发出这些请求并传回响应。图 10.3 显示了此代码结构的运行方式。
To accomplish that, you’ll create a utility class called NetworkService to handle the details of connecting to the internet and making HTTP requests. WeatherManager can then tell NetworkService to make those requests and pass back the response. Figure 10.3 shows how this code structure will operate.
Figure 10.3 How the networking code will be structured
为了实现这一点,显然WeatherManager需要能够访问NetworkService对象。您将通过在Managers中创建对象,然后在初始化各个管理器时将NetworkService对象注入其中来解决这个问题。这样,不仅WeatherManager会引用NetworkService,而且您稍后创建的任何其他管理器也会引用它。
For this to work, obviously WeatherManager will need to have access to the NetworkService object. You’re going to address this by creating the object in Managers and then injecting the NetworkService object into the various managers when they’re initialized. In this way, not only will WeatherManager have a reference to the NetworkService, but so will any other managers you create later.
要开始从第 9 章引入管理器代码架构,首先复制ManagerStatus和IGameManager(请记住,IGameManager是所有管理器必须实现的接口,而ManagerStatus是IGameManager使用的枚举)。您需要稍微修改IGameManager以适应新的NetworkService类,因此创建一个名为NetworkService的新脚本(删除:MonoBehaviour,否则暂时将其留空;您稍后会填写它),然后调整IGameManager。
To start bringing over the Managers code architecture from chapter 9, first copy over ManagerStatus and IGameManager (remember that IGameManager is the interface that all managers must implement, whereas ManagerStatus is an enum that IGameManager uses). You’ll need to modify IGameManager slightly to accommodate the new NetworkService class, so create a new script called NetworkService (delete :MonoBehaviour and otherwise leave it empty for now; you’ll fill it in later) and then adjust IGameManager.
清单 10.2 调整IGameManager以包含NetworkService
Listing 10.2 Adjusting IGameManager to include NetworkService
公共接口 IGameManager {
ManagerStatus 状态 {获取;}
void Startup(NetworkService 服务); ❶
}public interface IGameManager {
ManagerStatus status {get;}
void Startup(NetworkService service); ❶
}
❶ The startup function now takes one parameter: the injected object.
接下来我们创建WeatherManager来实现这个稍微调整过的界面。创建一个新的C#脚本。
Next let’s create WeatherManager to implement this slightly adjusted interface. Create a new C# script.
Listing 10.3 Initial script for WeatherManager
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类WeatherManager:MonoBehaviour,IGameManager {
公共 ManagerStatus 状态 {获取;私人设置;}
// 在此处添加云值(清单 10.8)
私有网络服务网络;
公共无效启动(NetworkService服务){
Debug.Log("天气管理器正在启动...");
网络=服务; ❶
状态 = ManagerStatus.已启动;
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WeatherManager : MonoBehaviour, IGameManager {
public ManagerStatus status {get; private set;}
// Add cloud value here (listing 10.8)
private NetworkService network;
public void Startup(NetworkService service) {
Debug.Log("Weather manager starting...");
network = service; ❶
status = ManagerStatus.Started;
}
}
❶ Store the injected NetworkService object.
WeatherManager的初始传递实际上什么也没做。目前,该类实现了IGameManager所需的最少内容:声明status属性从接口并实现Startup()函数。您将在接下来的几节中填写这个空框架。最后,从第 9 章复制Managers并对其进行调整以启动WeatherManager。
This initial pass at WeatherManager doesn’t really do anything. For now, the class implements the minimum amount that IGameManager requires: declare the status property from the interface and implement the Startup() function. You’ll fill in this empty framework over the next few sections. Finally, copy over Managers from chapter 9 and adjust it to start up WeatherManager.
清单 10.4管理器调整以初始化WeatherManager
Listing 10.4 Managers adjusted to initialize WeatherManager
使用System.Collections; 使用 System.Collections.Generic; 使用 UnityEngine; [RequireComponent(typeof(WeatherManager))] ❶ 公共类管理器:MonoBehaviour { 公共静态WeatherManager天气{获取;私人设置;} 私有列表<IGameManager> startSequence; 无效唤醒(){ 天气 = GetComponent<WeatherManager>(); 开始序列 = 新列表 <IGameManager>(); 开始序列.添加(天气); 启动协同程序(StartupManagers()); } 私有 IEnumerator StartupManagers() { NetworkService 网络 = new NetworkService(); ❷ foreach(startSequence 中的 IGameManager 管理器){ 管理器.启动(网络); ❸ } 产量返回 null; int numModules = 启动序列.计数; int 数量就绪 = 0; while (numReady < numModules) { int lastReady = numReady; 准备数量=0; foreach(startSequence 中的 IGameManager 管理器){ 如果 (manager.status == ManagerStatus.Started) { 数量就绪++; } } 如果 (numReady > lastReady) Debug.Log($"进度:{numReady}/{numModules}"); 产量返回 null; } Debug.Log("所有管理器已启动"); } }
using System.Collections; using System.Collections.Generic; using UnityEngine; [RequireComponent(typeof(WeatherManager))] ❶ public class Managers : MonoBehaviour { public static WeatherManager Weather {get; private set;} private List<IGameManager> startSequence; void Awake() { Weather = GetComponent<WeatherManager>(); startSequence = new List<IGameManager>(); startSequence.Add(Weather); StartCoroutine(StartupManagers()); } private IEnumerator StartupManagers() { NetworkService network = new NetworkService(); ❷ foreach (IGameManager manager in startSequence) { manager.Startup(network); ❸ } yield return null; int numModules = startSequence.Count; int numReady = 0; while (numReady < numModules) { int lastReady = numReady; numReady = 0; foreach (IGameManager manager in startSequence) { if (manager.status == ManagerStatus.Started) { numReady++; } } if (numReady > lastReady) Debug.Log($"Progress: {numReady}/{numModules}"); yield return null; } Debug.Log("All managers started up"); } }
❶ Require the new manager instead of player and inventory.
❷ Instantiate NetworkService to inject in all managers.
❸ Pass the network service to managers during startup.
这就是Managers代码架构所需的一切代码。如前几章所述,在场景中创建游戏管理器对象,然后将Managers和WeatherManager附加到空对象。即使管理器尚未执行任何操作,如果设置正确,您可以在控制台中看到启动消息。
And that’s everything needed codewise for the Managers code architecture. As you have in previous chapters, create the game managers object in the scene and then attach both Managers and WeatherManager to the empty object. Even though the manager isn’t doing anything yet, you can see startup messages in the console when it’s set up correctly.
呼,我们有不少样板代码需要处理!现在我们可以继续编写网络代码了。
Whew, we had quite a few boilerplate things to get out of the way! Now we can get on with writing the networking code.
网络服务 目前是一个空脚本,因此您可以在其中编写代码来发出 HTTP 请求。您需要了解的主要类是UnityWebRequest。Unity 提供了UnityWebRequest类与互联网通信。使用 URL 实例化请求对象将向该 URL 发送请求。
NetworkService is currently an empty script, so you can write code in it to make HTTP requests. The primary class you need to know about is UnityWebRequest. Unity provides the UnityWebRequest class to communicate with the internet. Instantiating a request object using a URL will send a request to that URL.
协程可以与UnityWebRequest类配合使用,等待请求完成。第 3 章介绍了协程,我们用它们暂停代码一段时间。回想一下那里给出的解释:协程是一种特殊的函数,它似乎在程序的后台运行,重复运行一部分然后返回到程序的其余部分。与 StartCoroutine ()方法一起使用时,yield关键字导致协程暂时暂停,交还程序流并在下一帧从该点再次继续。
Coroutines can work with the UnityWebRequest class to wait for the request to complete. Coroutines were introduced in chapter 3, where we used them to pause code for a set period of time. Recall the explanation given there: coroutines are special functions that seemingly run in the background of a program, in a repeated cycle of running partway and then returning to the rest of the program. When used along with the StartCoroutine() method, the yield keyword causes the coroutine to temporarily pause, handing back the program flow and picking up again from that point in the next frame.
在第 3 章中,协程在WaitForSeconds()处产生,这是一个导致函数暂停特定秒数的对象。发送请求时产生协程将暂停函数,直到该网络请求完成。此处的程序流程类似于在 Web 应用程序中进行异步 Ajax 调用:首先发送请求,然后继续执行程序的其余部分,一段时间后收到响应。
In chapter 3, the coroutines yielded at WaitForSeconds(), an object that caused the function to pause for a specific number of seconds. Yielding a coroutine when sending a request will pause the function until that network request completes. The program flow here is similar to making asynchronous Ajax calls in a web application: first you send a request, then you continue with the rest of the program, and after some time you receive a response.
That was the theory; now let’s write the code
全部好的,让我们在代码中实现这个东西。首先打开NetworkService脚本并用此列表的内容替换默认模板。
All right, let’s implement this stuff in our code. First open the NetworkService script and replace the default template with the contents of this listing.
清单 10.5 在NetworkService中发出 HTTP 请求
Listing 10.5 Making HTTP requests in NetworkService
使用系统;
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
使用 UnityEngine.Networking;
公共类网络服务{
私有 const 字符串 xmlApi = ❶
“http://api.openweathermap.org/data/2.5/weather?q=芝加哥,
➥我们&mode=xml&appid=APIKEY";
私有 IEnumerator CallAPI(字符串 url,Action<string> 回调) {
使用(UnityWebRequest请求= UnityWebRequest.Get(url)){ ❷
产生返回请求.SendWebRequest(); ❸
if (request.result == UnityWebRequest.Result.ConnectionError) { ❹
Debug.LogError($"网络问题:{request.error}"); ❹
} else if (request.result == UnityWebRequest.Result.ProtocolError) { ❹
Debug.LogError($"响应错误:{request.responseCode}");
} 别的 {
回调(request.downloadHandler.text); ❺
}
}
}
公共 IEnumerator GetWeatherXML(Action <string> 回调){
返回 CallAPI(xmlApi,回调); ❻
}
}using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
public class NetworkService {
private const string xmlApi = ❶
"http://api.openweathermap.org/data/2.5/weather?q=Chicago,
➥ us&mode=xml&appid=APIKEY";
private IEnumerator CallAPI(string url, Action<string> callback) {
using (UnityWebRequest request = UnityWebRequest.Get(url)) { ❷
yield return request.SendWebRequest(); ❸
if (request.result == UnityWebRequest.Result.ConnectionError) { ❹
Debug.LogError($"network problem: {request.error}"); ❹
} else if (request.result == UnityWebRequest.Result.ProtocolError) {❹
Debug.LogError($"response error: {request.responseCode}");
} else {
callback(request.downloadHandler.text); ❺
}
}
}
public IEnumerator GetWeatherXML(Action<string> callback) {
return CallAPI(xmlApi, callback); ❻
}
}
❷ Create UnityWebRequest object in GET mode.
❹ Check for errors in the response.
❺ Delegate can be called just like the original function.
❻ Yield cascades through coroutine methods that call each other.
警告动作类型(在“理解回调的工作原理”中解释)包含在System命名空间中;注意附加的using语句在脚本的顶部。不要忘记脚本中的这个细节!
warning The Action type (explained in “Understanding how the callback works”) is contained in the System namespace; notice the additional using statements at the top of the script. Don’t forget this detail in your scripts!
记住前面解释的代码设计:WeatherManager将告诉NetworkService去获取数据。所有这些代码实际上尚未运行;您正在设置稍后将由WeatherManager调用的代码。要探索此代码清单,让我们从底部开始并按我们的方式进行向上。
Remember the code design explained earlier: WeatherManager will tell NetworkService to go fetch data. All this code doesn’t actually run yet; you’re setting up code that will be called by WeatherManager a bit later. To explore this code listing, let’s start at the bottom and work our way up.
Writing coroutine methods that cascade through each other
获取WeatherXML()是外部代码可用于告诉NetworkService发出 HTTP 请求的协程方法。请注意,此函数的返回类型为IEnumerator ;协程中使用的方法必须将IEnumerator声明为返回类型。
GetWeatherXML()is the coroutine method that outside code can use to tell NetworkService to make an HTTP request. Notice that this function has IEnumerator for its return type; methods used in coroutines must have IEnumerator declared as the return type.
乍一看, GetWeatherXML()没有yield语句可能有点奇怪。协程由yield语句暂停,这意味着每个协程都必须在某处产生yield。事实证明,产生yield可以通过多种方法级联。如果初始协程方法本身调用另一个方法,而另一个方法在执行过程中产生部分yield,则协程将在第二个方法内暂停并从那里恢复。因此,CallAPI()中的yield语句暂停在GetWeatherXML()中启动的协程;图10.4显示了此代码流。
It might look odd at first that GetWeatherXML() doesn’t have a yield statement. Coroutines are paused by the yield statement, which implies that every coroutine must yield somewhere. It turns out that the yielding can cascade through multiple methods. If the initial coroutine method itself calls another method, and that other method yields part of the way through, then the coroutine will pause inside that second method and resume there. Thus, the yield statement in CallAPI() pauses the coroutine that was started in GetWeatherXML(); figure 10.4 shows this code flow.
Figure 10.4 How the network coroutine works
The next potential head-scratcher is the callback parameter of type Action.
Understanding how the callback works
什么时候协程启动后,方法被调用,参数为回调,回调为Action类型。但是Action是什么?
When the coroutine is started, the method is called with a parameter called callback, and callback has the Action type. But what is an Action?
定义Action类型是委托(C# 有几种委托方法,但这是最简单的)。委托是对其他方法/函数的引用。它们允许您将函数(或者更确切地说,指向函数的指针)存储在变量中,并将该函数作为参数传递给另一个函数。
DEFINITION The Action type is a delegate (C# has a few approaches to delegates, but this one is the simplest). Delegates are references to some other method/function. They allow you to store the function (or rather, a pointer to the function) in a variable and to pass that function as a parameter to another function.
如果您不熟悉委托的概念,请意识到它们使您能够传递函数,就像传递数字和字符串一样。没有委托,您无法传递函数以供稍后调用 - 您只能立即直接调用该函数。使用委托,您可以告诉代码有关稍后调用的其他方法的信息。这对于许多目的都很有用,尤其是对于实现回调函数。
If you’re unfamiliar with the concept of delegates, realize that they enable you to pass around functions just as you do numbers and strings. Without delegates, you can’t pass around functions to call later—you can only directly call the function immediately. With delegates, you can tell code about other methods to call later. This is useful for many purposes, especially for implementing callback functions.
定义回调是用于与调用对象进行通信的函数。对象 A 可以告诉对象 B A 中的一种方法。B 稍后可以调用 A 的方法与 A 进行通信。
DEFINITION A callback is a function used to communicate back to the calling object. Object A could tell Object B about one of the methods in A. B could later call A’s method to communicate back to A.
例如,在本例中,回调用于在等待 HTTP 请求完成后传回响应数据。在CallAPI()中,代码首先发出 HTTP 请求,然后放弃直到该请求完成,最后使用回调()发回响应。
In this case, for example, the callback is used to communicate the response data back after waiting for the HTTP request to complete. In CallAPI(), the code first makes an HTTP request, then yields until that request completes, and finally uses callback() to send back the response.
请注意与Action关键字一起使用的<>语法;尖括号中写的类型声明了适合此Action所需的参数。换句话说,此Action指向的函数必须采用与声明类型匹配的参数。在本例中,参数是一个字符串,因此回调方法必须具有如下签名:
Note the <> syntax used with the Action keyword; the type written in the angle brackets declares the parameters required to fit this Action. In other words, the function this Action points to must take parameters matching the declared type. In this case, the parameter is a single string, so the callback method must have a signature like this:
方法名称(字符串值)
MethodName(string value)
在看到回调的实际运行之后,您可能对回调的概念更感兴趣,就像清单 10.6 中那样;这个初步的解释是为了让您在看到附加代码时认识到发生了什么。
The concept of a callback may make more sense after you’ve seen it in action, which you will in listing 10.6; this initial explanation is so that you’ll recognize what’s going on when you see that additional code.
清单 10.5 的其余部分非常简单。请求对象是在using语句中创建的,因此一旦我们使用该对象完成,对象的内存就会被清理。条件检查 HTTP 响应中的错误。有两种错误:请求可能由于互联网连接不良而失败,或者返回的响应可能包含错误代码。使用要向其发出请求的 URL 声明一个const值。(顺便说一句,您应该将末尾的APIKEY替换为您的 Open天气 API 密钥。
The rest of listing 10.5 is pretty straightforward. The request object is created inside a using statement so that the object’s memory will be cleaned up once we’re done with that object. The conditional checks for errors in the HTTP response. There are two kinds of errors: the request could’ve failed because of a bad internet connection, or the response returned could have an error code. A const value is declared with the URL to make the request to. (Incidentally, you should replace APIKEY at the end with your OpenWeather API key.)
Making use of the Networking code
那将代码包装在NetworkService中。现在让我们在WeatherManager中使用NetworkService。
That wraps up the code in NetworkService. Now let’s use NetworkService in WeatherManager.
清单 10.6 调整WeatherManager以使用NetworkService
Listing 10.6 Adjusting WeatherManager to use NetworkService
...
公共无效启动(NetworkService服务){
Debug.Log("天气管理器正在启动...");
网络=服务;
启动协同程序(network.GetWeatherXML(OnXMLDataLoaded)); ❶
状态 = ManagerStatus.Initializing; ❷
}
public void OnXMLDataLoaded(string data) { ❸
调试.日志(数据);
状态 = ManagerStatus.已启动;
}
......
public void Startup(NetworkService service) {
Debug.Log("Weather manager starting...");
network = service;
StartCoroutine(network.GetWeatherXML(OnXMLDataLoaded)); ❶
status = ManagerStatus.Initializing; ❷
}
public void OnXMLDataLoaded(string data) { ❸
Debug.Log(data);
status = ManagerStatus.Started;
}
...
❶ Start loading data from the internet.
❷ Instead of Started, make the status Initializing.
❸ Callback method after the data is loaded
该管理器中的代码主要进行了三处更改:启动协同程序从互联网下载数据、设置不同的启动状态以及定义回调方法来接收响应。
Three primary changes are made to the code in this manager: starting a coroutine to download data from the internet, setting a different startup status, and defining a callback method to receive the response.
启动协程很简单。协程背后的大部分复杂性都在NetworkService中处理,因此您只需在此处调用StartCoroutine()即可。然后设置不同的启动状态,因为管理器尚未完成初始化;它需要在启动完成之前从互联网接收数据。
Starting the coroutine is simple. Most of the complexity behind coroutines was handled in NetworkService, so calling StartCoroutine() is all you need to do here. Then you set a different startup status, because the manager isn’t finished initializing; it needs to receive data from the internet before startup is complete.
警告始终使用StartCoroutine()启动网络方法;不要只是正常调用该函数。这一点很容易被忘记,因为在协程之外创建请求对象不会产生任何类型的编译器错误。
WARNING Always start networking methods by using StartCoroutine(); don’t just call the function normally. This can be easy to forget because creating request objects outside of a coroutine doesn’t generate any sort of compiler error.
调用StartCoroutine()方法时,需要调用用作参数的方法。也就是说,实际输入括号(),而不是仅提供函数名称。在本例中,协程方法需要一个回调函数作为其一个参数,因此让我们定义该函数。我们将使用OnXMLDataLoaded()进行回调;请注意,此方法有一个字符串参数,它符合NetworkService中的Action<string>声明。回调函数现在不做很多事情;调试行只是将接收到的数据打印到控制台以验证数据是否正确接收。然后,函数的最后一行将管理器的启动状态更改为表示它已完全启动。
When you call the StartCoroutine() method, you need to invoke the method used as a parameter. That is, actually type the parentheses—()—and don’t provide only the name of the function. In this case, the coroutine method needs a callback function as its one parameter, so let’s define that function. We’ll use OnXMLDataLoaded() for the callback; notice that this method has a string parameter, which fits the Action<string> declaration from NetworkService. The callback function doesn’t do a lot right now; the debug line simply prints the received data to the console to verify that the data was received correctly. Then the last line of the function changes the startup status of the manager to say that it’s completely started up.
单击“播放”运行代码。假设您有稳定的互联网连接,您应该会看到控制台中出现一堆数据。这些数据只是一个长字符串,但该字符串的格式是特定的,我们可以将其设置为使用的。
Click Play to run the code. Assuming you have a solid internet connection, you should see a bunch of data appear in the console. This data is simply a long string, but the string is formatted in a specific way that we can make use of.
数据以长字符串形式存在的字符串中通常会嵌入单独的信息位。您可以通过解析字符串来提取这些信息位。
Data that exists as a long string usually has individual bits of information embedded within the string. You extract those bits of information by parsing the string.
DEFINITION Parsing means analyzing a chunk of data and dividing it into separate pieces of information.
要解析字符串,需要将其格式化为允许您(或者更确切地说,解析器代码)识别单独部分的方式。通常使用几种标准格式通过互联网传输数据;最常见的标准格式之一是XML。
To parse the string, it needs to be formatted in a way that allows you (or rather, the parser code) to identify separate pieces. A couple of standard formats are commonly used to transfer data over the internet; one of the most common standard formats is XML.
定义 XML代表可扩展标记语言。它是一组以结构化方式对文档进行编码的规则,类似于 HTML 网页。
DEFINITION XML stands for Extensible Markup Language. It’s a set of rules for encoding documents in a structured way, similar to HTML web pages.
幸运的是,Unity(或者更确切地说是 Mono,Unity 内置的代码框架)提供了解析 XML 的功能。我们请求的天气数据采用 XML 格式,因此我们将向WeatherManager添加代码来解析响应并提取云量。将 URL 放入 Web 浏览器中以查看响应数据;那里有很多,但我们只对包含类似<clouds value="40" name="scatteredclouds "/>的节点感兴趣。
Fortunately, Unity (or rather Mono, the code framework built into Unity) provides functionality for parsing XML. The weather data we requested is formatted in XML, so we’re going to add code to WeatherManager to parse the response and extract the cloudiness. Put the URL into a web browser to see the response data; there’s a lot there, but we’re interested only in the node that contains something like <clouds value="40" name="scattered clouds"/>.
除了添加代码来解析 XML 之外,我们还将使用与第 7 章中相同的消息系统。这是因为一旦下载并解析了天气数据,我们仍然需要将此事通知场景。创建一个名为Messenger 的脚本,并将来自https://github.com/jhocking/from-unity-wiki/blob/main/Messenger.cs的代码粘贴进去。
In addition to adding code to parse XML, we’re going to use the same messenger system as we did in chapter 7. That’s because once the weather data is downloaded and parsed, we still need to inform the scene about that. Create a script called Messenger and paste in the code from https://github.com/jhocking/from-unity-wiki/blob/main/ Messenger.cs.
然后你需要创建一个名为GameEvent的脚本。如第 7 章所述,此消息系统非常适合提供一种与程序其余部分进行事件通信的解耦方式。
Then you need to create a script called GameEvent. As explained in chapter 7, this messenger system is great for providing a decoupled way of communicating events to the rest of the program.
公共静态类 GameEvent {
公共 const 字符串 WEATHER_UPDATED = “WEATHER_UPDATED”;
}public static class GameEvent {
public const string WEATHER_UPDATED = "WEATHER_UPDATED";
}
Once the messenger system is in place, adjust WeatherManager.
清单 10.8 在WeatherManager中解析 XML
Listing 10.8 Parsing XML in WeatherManager
使用系统; 使用System.Xml; ❶ ... 公共浮点云值{获取;私有集合;} ❷ ... 公共无效OnXMLDataLoaded(字符串数据){ XmlDocument doc = 新的 XmlDocument(); doc.LoadXml(数据); ❸ XmlNode根=doc.DocumentElement; XmlNode node = root.SelectSingleNode("云"); ❹ 字符串值 = 节点.Attributes["value"].Value; cloudValue = Convert.ToInt32(value) / 100f; ❺ Debug.Log($"值:{cloudValue}"); Messenger.Broadcast(GameEvent.WEATHER_UPDATED); ❻ 状态 = ManagerStatus.已启动; } ...
using System; using System.Xml; ❶ ... public float cloudValue {get; private set;} ❷ ... public void OnXMLDataLoaded(string data) { XmlDocument doc = new XmlDocument(); doc.LoadXml(data); ❸ XmlNode root = doc.DocumentElement; XmlNode node = root.SelectSingleNode("clouds"); ❹ string value = node.Attributes["value"].Value; cloudValue = Convert.ToInt32(value) / 100f; ❺ Debug.Log($"Value: {cloudValue}"); Messenger.Broadcast(GameEvent.WEATHER_UPDATED); ❻ status = ManagerStatus.Started; } ...
❶ Be sure to add needed using statements.
❷ Cloudiness is modified internally but read-only elsewhere.
❸ Parse XML into a searchable structure.
❹ Pull out a single node from the data.
❺ Convert the value to a 0-1 float.
❻ Broadcast message to inform the other scripts.
您可以看到,最重要的更改是在OnXMLDataLoaded()中进行的。以前,此方法只是将数据记录到控制台以验证数据是否正确传输。此清单添加了大量代码来解析 XML。
You can see that the most important changes were made inside OnXMLDataLoaded(). Previously, this method simply logged the data to the console to verify that data was coming through correctly. This listing adds a lot of code to parse the XML.
首先创建一个新的空 XML 文档;这是一个空容器,您可以用解析后的 XML 结构填充它。下一行将数据字符串解析为 XML 文档包含的结构。然后我们从 XML 树的根开始,以便所有内容都可以在后续代码中搜索该树。
First create a new empty XML document; this is an empty container that you can fill with a parsed XML structure. The next line parses the data string into a structure contained by the XML document. Then we start at the root of the XML tree so that everything can search up the tree in subsequent code.
此时,您可以在 XML 结构中搜索节点以提取单个信息。在本例中,<clouds>是我们唯一感兴趣的节点。在 XML 文档中找到该节点,然后从该节点提取值属性。此数据将云值定义为 0-100 的整数,但为了稍后调整场景,我们将需要将其转换为 0-1 的浮点数。转换它是添加到代码中的简单数学运算。
At this point, you can search for nodes within the XML structure to pull out individual bits of information. In this case, <clouds> is the only node we’re interested in. Find that node in the XML document and then extract the value attribute from that node. This data defines the cloud value as a 0-100 integer, but we’re going to need it as a 0-1 float in order to adjust the scene later. Converting that is a simple bit of math added to the code.
最后,从完整数据中提取出云量值后,广播一条消息,告知天气数据已更新。目前,没有任何设备在监听该消息,但广播者不需要知道有关监听者的任何信息(事实上,这正是解耦消息系统的全部意义所在)。稍后,我们将向场景中添加一个监听者。
Finally, after extracting out the cloudiness value from the full data, broadcast a message that the weather data has been updated. Currently, nothing is listening for that message, but the broadcaster doesn’t need to know anything about listeners (indeed, that’s the entire point of a decoupled messenger system). Later, we’ll add a listener to the scene.
太棒了——我们已经编写了解析 XML 数据的代码!但在我们将这个值应用到可见场景之前,我想先介绍一下数据的另一个选项转移。
Great—we’ve written code to parse XML data! But before we move on to applying this value to the visible scene, I want to go over another option for data transfer.
前继续项目的下一步,让我们探索传输数据的替代格式。 XML 是通过互联网传输数据的一种常见格式;另一种常见格式是JSON。
Before continuing to the next step in the project, let’s explore an alternative format for transferring data. XML is one common format for data transferred over the internet; another common one is JSON.
定义 JSON代表JavaScript 对象表示法。JSON 的目的与 XML 类似,旨在成为一种轻量级替代方案。尽管 JSON 的语法最初源自 JavaScript,但该格式并不特定于语言,并且可轻松用于各种编程语言。
DEFINITION JSON stands for JavaScript Object Notation. Similar in purpose to XML, JSON was designed to be a lightweight alternative. Although the syntax for JSON was originally derived from JavaScript, the format is not language-specific and is readily used with a variety of programming languages.
与 XML 不同,Mono 没有自带这种格式的解析器。幸运的是,有许多优秀的 JSON 解析器可用。Unity 本身提供了一个JsonUtility类而外部开发的选项包括来自 Newtonsoft 的 Json.NET。我通常在游戏中使用 Json.NET,因为 Newtonsoft 的库在 Unity 之外的整个 .NET 生态系统中被广泛使用。它可以使用 Unity 的新包管理器系统进行安装,这就是它在示例项目中的安装方式。
Unlike XML, Mono doesn’t come with a parser for this format. Fortunately, numerous good JSON parsers are available. Unity itself provides a JsonUtility class, while externally developed options include Json.NET from Newtonsoft. I generally use Json.NET in my games, because Newtonsoft’s library is widely used outside Unity in the whole .NET ecosystem. It can be installed using Unity’s new Package Manager system, and that’s how it’s installed in the sample project.
警告Json.NET 实际上已多次为 Unity 打包,本书使用来自 jilleJr 的包。但是,最近 Unity 将 Json.NET 打包为 com.unity.nuget.newtonsoft-json,并将其用作其他包的依赖项。因此,如果您安装了其中一个其他包(例如版本控制),则您的项目中已经有 Json.NET,再次尝试安装 Json.NET 将导致错误。最简单的检查方法是展开 Project 视图中的 Packages 文件夹(位于 Assets 下方)并查找 Newtonsoft Json。
WARNING Json.NET has actually been packaged for Unity multiple times, and this book uses the package from jilleJr. However, recently Unity packaged Json.NET as com.unity.nuget.newtonsoft-json, and uses that as a dependency for other packages. Thus, if you have one of those other packages installed (such as Version Control), then you already have Json.NET in your project, and trying to install Json.NET a second time will cause errors. The easiest way to check is to expand the Packages folder (below Assets) in the Project view and look for Newtonsoft Json.
GitHub 页面:http: //mng.bz/7l4y有多个关于如何安装的部分,其中“通过 Pure UPM 安装”解释了我们需要的步骤。正如第 1 章中提到的,Unity 包管理器(UPM)最容易与 Unity 自己制作的包一起使用。但是,UPM 也越来越多地得到外部包作者的支持;例如,第 4 章中提到的 glTF 包就是以这种方式安装的。虽然 Unity 制作的包在包管理器窗口中列出并可在那里选择,但需要通过调整清单文本文件来安装外部创建的包。
The GitHub page at http://mng.bz/7l4y has multiple sections about how to install, and “Installation via Pure UPM” explains the steps we need. As mentioned way back in chapter 1, the Unity Package Manager (UPM) is easiest to use with packages made by Unity itself. However, UPM is increasingly supported by external package authors as well; for example, the glTF package mentioned in chapter 4 is installed this way. While packages made by Unity are listed in the Package Manager window and can be selected there, externally created packages need to be installed by adjusting the manifest text file.
根据 GitHub 页面的说明,导航到计算机上的 Unity 项目文件夹,打开其中的 Packages 文件夹,然后在任何文本编辑器中打开 manifest.json。GitHub 上的安装文档列出了要粘贴到包清单中的所有文本,因此请执行此操作。安装包总是涉及在依赖项块中添加条目;此外,一些包(例如,此 JSON 库)还将包含scopedRegistries供您添加。返回 Unity,新包需要一点时间才能下载。
As explained by the GitHub page, navigate to the Unity project’s folder on your computer, open the Packages folder in there, and then open manifest.json in any text editor. The installation documentation on GitHub lists all the text to paste into the package manifest, so do that. Installing a package always involves adding an entry in the dependencies block; in addition, some packages (for example, this JSON library) will also have scopedRegistries for you to add. Return to Unity, where it will take a moment for the new package to download.
现在您可以使用此库来解析 JSON 数据。我们一直从 OpenWeather API 获取 XML,但实际上,OpenWeather 也可以发送 JSON 格式的相同数据。为此,请修改NetworkService以请求 JSON。
Now you can use this library to parse JSON data. We’ve been getting XML from the OpenWeather API, but as it happens, OpenWeather can also send the same data formatted as JSON. To do that, modify NetworkService to request JSON.
清单 10.9 使NetworkService请求使用 JSON 而不是 XML
Listing 10.9 Making NetworkService request JSON instead of XML
...
私有 const string jsonApi = ❶
“http://api.openweathermap.org/data/2.5/weather?q=芝加哥,us&appid=APIKEY”;
...
公共 IEnumerator GetWeatherJSON(Action <string> 回调){
返回 CallAPI(jsonApi,回调);
}
......
private const string jsonApi = ❶
"http://api.openweathermap.org/data/2.5/weather?q=Chicago,us&appid=APIKEY";
...
public IEnumerator GetWeatherJSON(Action<string> callback) {
return CallAPI(jsonApi, callback);
}
...
❶ The URL is slightly different this time.
这与下载 XML 数据的代码基本相同,只是 URL 略有不同。此请求返回的数据具有相同的值,但格式不同。这次我们要查找的是像"clouds":{"all":40}这样的块。
This is pretty much the same as the code to download XML data, except that the URL is slightly different. The data returned from this request has the same values, but it’s formatted differently. This time we’re looking for a chunk like "clouds":{"all":40}.
这次不需要大量的额外代码。这是因为我们将请求代码设置为很好地划分的单独函数,因此每个后续 HTTP 请求都很容易添加。太棒了!现在让我们修改WeatherManager以请求 JSON 数据而不是 XML。
There wasn’t a ton of additional code required this time. That’s because we set up the code for requests into nicely parceled separate functions, so every subsequent HTTP request will be easy to add. Nice! Now let’s modify WeatherManager to request JSON data instead of XML.
清单 10.10 修改WeatherManager以请求 JSON
Listing 10.10 Modifying WeatherManager to request JSON
... 使用 Newtonsoft.Json.Linq; ❶ ... 公共无效启动(NetworkService服务){ Debug.Log("天气管理器正在启动..."); 网络=服务; 启动协同程序(network.GetWeatherJSON(OnJSONDataLoaded)); ❷ 状态 = 管理器状态.初始化; } ... 公共无效OnJSONDataLoaded(字符串数据){ JObject 根 = JObject.Parse(数据); ❸ JTokenclouds = root["clouds"]; ❹cloudValue = (float)clouds["all"] / 100f; ❹Debug.Log($"Value: {cloudValue}") ; ❹ Messenger.Broadcast(GameEvent.WEATHER_UPDATED); ❹ 状态 = ManagerStatus.已启动; } ...
... using Newtonsoft.Json.Linq; ❶ ... public void Startup(NetworkService service) { Debug.Log("Weather manager starting..."); network = service; StartCoroutine(network.GetWeatherJSON(OnJSONDataLoaded)); ❷ status = ManagerStatus.Initializing; } ... public void OnJSONDataLoaded(string data) { JObject root = JObject.Parse(data); ❸ JToken clouds = root["clouds"]; ❹ cloudValue = (float)clouds["all"] / 100f; ❹ Debug.Log($"Value: {cloudValue}"); ❹ Messenger.Broadcast(GameEvent.WEATHER_UPDATED); ❹ status = ManagerStatus.Started; } ...
❶ Be sure to add the needed using statement.
❸ Instead of an XML container, parse into a JSON object.
❹ Syntax has changed, but this code is still doing the same things.
如您所见,处理 JSON 的代码与处理 XML 的代码类似。唯一的区别是数据被解析为 JSON 对象,而不是 XML 文档容器。
As you can see, the code for working with JSON looks similar to the code for XML. The only real difference is that the data is parsed into a JSON object instead of an XML document container.
注意: Json.NET 提供了多种解析数据的方法,这里使用的替代方法称为JSON Linq。这种替代方法不需要太多设置,这对于像这样的小示例来说很方便。但是,主要方法需要首先创建一个新类,其中包含与 JSON 数据结构相似的字段。然后使用命令JsonConvert.DeserializeObject填充该类。
NOTE Json.NET provides multiple approaches to parsing the data, and the alternative used here is referred to as JSON Linq. This alternative approach doesn’t require as much setup, which is convenient for a small example like this. The main approach, however, requires first creating a new class with fields that mirror the structure of the JSON data. The data then populates this class by using the command JsonConvert.DeserializeObject.
定义反 序列化含义与parse基本相同,只是暗示要从数据中创建代码对象。这与serialize相反,意思是将代码对象编码成可以传输和存储的形式,比如JSON字符串。
DEFINITION Deserialize means pretty much the same thing as parse, only with the implication that a code object is being created out of the data. This is the reverse of serialize, which means to encode a code object into a form that can be transferred and stored, such as a JSON string.
除了语法不同之外,所有步骤都相同。从数据块中提取值(出于某种原因,该值一直被调用,但这只是 API 的一个怪癖),进行一些简单的数学运算以将值转换为 0-1 浮点数,并广播更新消息。完成后,是时候将值应用于可见场景。
Aside from the different syntax, all the steps are the same. Extract the value from the data chunk (for some reason, the value is called all this time, but that’s just a quirk of the API), do some simple math to convert the value to a 0-1 float, and broadcast an update message. With that done, it’s time to apply the value to the visible scene.
不管数据的格式究竟是如何的,一旦从响应数据中提取出云量值,我们就可以在SetOvercast()方法中使用该值WeatherController的。无论是 XML 还是 JSON,数据字符串最终都会被解析为一系列单词和数字。SetOvercast ()方法接受一个数字作为参数。在第 9.1.2 节中,我们使用了一个每帧递增的数字,但我们也可以很容易地使用天气 API 返回的数字。以下是完整的WeatherController脚本经过修改后。
Regardless of exactly how the data is formatted, once the cloudiness value is extracted from the response data, we can use that value in the SetOvercast() method of WeatherController. Whether XML or JSON, the data string ultimately gets parsed into a series of words and numbers. The SetOvercast() method takes a number as a parameter. In section 9.1.2, we used a number incremented every frame, but we could just as easily use the number returned by the weather API. This shows the full WeatherController script again, after modifications.
清单 10.11对下载的天气数据做出反应的WeatherController
Listing 10.11 WeatherController that reacts to downloaded weather data
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类WeatherController:MonoBehaviour {
[SerializeField] 材质天空;
[SerializeField] 光太阳;
私人浮动全强度;
void OnEnable() { ❶
Messenger.添加监听器(GameEvent.WEATHER_UPDATED,OnWeatherUpdated);
}
无效OnDisable(){
Messenger.RemoveListener(GameEvent.WEATHER_UPDATED,OnWeatherUpdated);
}
无效开始(){
全强度 = 太阳强度;
}
私有无效OnWeatherUpdated(){
设置天气预报(Managers.Weather.cloudValue); ❷
}
私有 void SetOvercast(浮点值) {
天空.SetFloat(“_Blend”,值);
太阳强度 = 全强度 - (全强度 * 值);
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WeatherController : MonoBehaviour {
[SerializeField] Material sky;
[SerializeField] Light sun;
private float fullIntensity;
void OnEnable() { ❶
Messenger.AddListener(GameEvent.WEATHER_UPDATED, OnWeatherUpdated);
}
void OnDisable() {
Messenger.RemoveListener(GameEvent.WEATHER_UPDATED, OnWeatherUpdated);
}
void Start() {
fullIntensity = sun.intensity;
}
private void OnWeatherUpdated() {
SetOvercast(Managers.Weather.cloudValue); ❷
}
private void SetOvercast(float value) {
sky.SetFloat("_Blend", value);
sun.intensity = fullIntensity - (fullIntensity * value);
}
}
❷ Use the cloudiness value from WeatherManager.
请注意,更改不仅仅是添加;删除了几段测试代码。具体来说,我们删除了每帧递增的局部云量值;我们不再需要它了,因为我们将使用WeatherManager中的值。
Notice that the changes aren’t only additions; several bits of test code got removed. Specifically, we removed the local cloudiness value that was incremented every frame; we don’t need that anymore, because we’ll use the value from WeatherManager.
在OnEnable() / OnDisable()中添加和删除侦听器(这些是MonoBehaviour的函数,在对象打开或关闭时调用)。此侦听器是广播消息系统的一部分,并在收到该消息时调用OnWeatherUpdated() 。 OnWeatherUpdated()从WeatherManager检索云量值,并使用该值调用SetOvercast()。这样,场景的外观由下载的天气数据控制。
A listener gets added and removed in OnEnable()/OnDisable() (these are the functions of MonoBehaviour called when the object is turned on or off). This listener is part of the broadcast messaging system and calls OnWeatherUpdated() when that message is received. OnWeatherUpdated() retrieves the cloudiness value from WeatherManager and calls SetOvercast() using that value. In this way, the appearance of the scene is controlled by downloaded weather data.
现在运行场景,您将看到天空根据天气数据中的云量进行更新。您可能会看到请求天气需要一些时间;在真正的游戏中,您可能希望将场景隐藏在加载屏幕后面,直到天空更新。
Run the scene now and you’ll see the sky update according to the cloudiness in the weather data. You may see it take time to request the weather; in a real game, you’d probably want to hide the scene behind a loading screen until the sky updates.
现在你已经知道如何从互联网上获取数字和字符串数据,让我们用一个图像。
Now that you know how to get numerical and string data from the internet, let’s do the same thing with an image.
虽然Web API 的响应几乎总是 XML 或 JSON 格式的文本字符串,但许多其他类型的数据也会通过互联网传输。除了文本数据之外,请求的最常见数据类型是图像。UnityWebRequest对象也可以用来下载图像。
Although the responses from a web API are almost always text strings formatted in XML or JSON, many other sorts of data are transferred over the internet. Besides text data, the most common kind of data requested is images. The UnityWebRequest object can be used to download images too.
您将通过创建一个显示从互联网下载的图像的广告牌来学习此任务。您需要编写两个步骤的代码:下载要显示的图像并将该图像应用于广告牌对象。作为第三步,您将改进代码,以便将图像存储起来以在多个广告牌上使用。
You’re going to learn about this task by creating a billboard that displays an image downloaded from the internet. You need to code two steps: downloading an image to display and applying that image to the billboard object. As a third step, you’ll improve the code so that the image will be stored to use on multiple billboards.
第一的让我们编写代码来下载图像。您将下载一些公共领域的风景摄影作品(见图 10.5)进行测试。下载的图像暂时不会显示在广告牌上;我将在下一节向您展示一个显示图像的脚本,但在此之前,让我们先准备好检索图像的代码。
First let’s write the code to download an image. You’re going to download some public domain landscape photography (see figure 10.5) to test with. The downloaded image won’t be visible on the billboard yet; I’ll show you a script to display the image in the next section, but before that, let’s get the code in place that will retrieve the image.
Figure 10.5 Image of Moraine Lake in Banff National Park, Canada
下载图片的代码架构与下载数据的架构非常相似。一个新的管理模块(称为ImagesManager)将负责显示下载的图片。再次强调,连接互联网和发送 HTTP 请求的细节将在NetworkService中处理,ImagesManager将调用NetworkService来为其下载图片。
The code architecture for downloading an image looks much the same as the architecture for downloading data. A new manager module (called ImagesManager) will be in charge of downloaded images to be displayed. Once again, the details of connecting to the internet and sending HTTP requests will be handled in NetworkService, and ImagesManager will call upon NetworkService to download images for it.
代码中第一个添加的是NetworkService。此清单将图像下载添加到该脚本中。
The first addition to the code is in NetworkService. This listing adds image downloading to that script.
Listing 10.12 Downloading an image in NetworkService
... 私有 const string webImage = ❶ “http://upload.wikimedia.org/wikipedia/commons/c/c5/Moraine_Lake_17092005.jpg”; ... 公共 IEnumerator DownloadImage(Action <Texture2D> 回调){ ❷ UnityWebRequest 请求 = UnityWebRequestTexture.GetTexture(webImage); 产生返回请求.SendWebRequest(); 回调(DownloadHandlerTexture.GetContent(请求)); ❸ } ...
... private const string webImage = ❶ "http://upload.wikimedia.org/wikipedia/commons/c/c5/Moraine_Lake_17092005.jpg"; ... public IEnumerator DownloadImage(Action<Texture2D> callback) { ❷ UnityWebRequest request = UnityWebRequestTexture.GetTexture(webImage); yield return request.SendWebRequest(); callback(DownloadHandlerTexture.GetContent(request)); ❸ } ...
❶ Put this const up near the top with the other URLs.
❷ This callback takes a Texture2D instead of a string.
❸使用 DownloadHandler 实用程序检索已下载的图像。
❸ Retrieve the downloaded image by using the DownloadHandler utility.
下载图片的代码与下载数据的代码几乎完全相同。主要区别在于回调方法的类型;请注意,这次回调采用的是 Texture2D 而不是字符串。这是因为您要发回相关的响应:您之前下载的是数据字符串 — 现在您正在下载图片。此清单包含新ImagesManager的代码。创建一个新脚本并输入此代码。
The code that downloads an image looks almost identical to the code for downloading data. The primary difference is the type of callback method; note that the callback takes a Texture2D this time instead of a string. That’s because you’re sending back the relevant response: you downloaded a string of data previously—now you’re downloading an image. This listing contains code for the new ImagesManager. Create a new script and enter this code.
清单 10.13 创建ImagesManager来检索和存储图像
Listing 10.13 Creating ImagesManager to retrieve and store images
使用系统;
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 ImagesManager : MonoBehaviour,IGameManager {
公共 ManagerStatus 状态 {获取;私人设置;}
私有网络服务网络;
私有Texture2D webImage; ❶
公共无效启动(NetworkService服务){
Debug.Log("图像管理器正在启动...");
网络=服务;
状态 = ManagerStatus.已启动;
}
公共无效GetWebImage(Action <Texture2D>回调){
如果 (webImage == null) { ❷
启动协同程序(网络.下载图像(回调));
}
别的 {
回调(webImage); ❸
}
}
}using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ImagesManager : MonoBehaviour, IGameManager {
public ManagerStatus status {get; private set;}
private NetworkService network;
private Texture2D webImage; ❶
public void Startup(NetworkService service) {
Debug.Log("Images manager starting...");
network = service;
status = ManagerStatus.Started;
}
public void GetWebImage(Action<Texture2D> callback) {
if (webImage == null) { ❷
StartCoroutine(network.DownloadImage(callback));
}
else {
callback(webImage); ❸
}
}
}
❶ Variable to store the downloaded image
❷ Check whether the image is already stored.
❸ Invoke the callback right away (don’t download) if there’s a stored image.
这段代码中最有趣的部分是GetWebImage();此脚本中的其他所有内容都由实现管理器接口的标准属性和方法组成。调用GetWebImage()时,它将返回(通过回调函数)Web 图像。首先,它将检查webImage是否已存储图像。如果没有,它将调用网络调用来下载图像。如果webImage已存储图像,GetWebImage()将返回存储的图像(而不是重新下载图像)。
The most interesting part of this code is GetWebImage(); everything else in this script consists of standard properties and methods that implement the manager interface. When GetWebImage() is called, it’ll return (via a callback function) the web image. First, it’ll check whether webImage already has a stored image. If not, it’ll invoke the network call to download the image. If webImage already has a stored image, GetWebImage() will send back the stored image (rather than downloading the image anew).
注意:目前,下载的图像永远不会被存储,这意味着webImage将始终为空。指定当webImage不为空时要做什么的代码已经到位,因此您将在以下部分调整代码以存储该图像。此调整位于单独的部分,因为它涉及一些棘手的代码技巧。
NOTE Currently, the downloaded image is never being stored, which means webImage will always be empty. Code that specifies what to do when webImage is not empty is already in place, so you’ll adjust the code to store that image in the following sections. This adjustment is in a separate section because it involves some tricky code wizardry.
当然,与所有管理器模块一样,ImagesManager需要添加到Managers中,此清单详细说明了添加的内容。
Of course, just like all manager modules, ImagesManager needs to be added to Managers, and this listing details the additions.
Listing 10.14 Adding the new manager to Managers
...
[RequireComponent(typeof(ImagesManager))]
...
公共静态 ImagesManager 图像 {获取;私有设置;}
...
无效唤醒(){
天气 = GetComponent<WeatherManager>();
图像 = GetComponent<ImagesManager>();
开始序列 = 新列表 <IGameManager>();
开始序列.添加(天气);
开始序列.添加(图像);
启动协同程序(StartupManagers());
}
......
[RequireComponent(typeof(ImagesManager))]
...
public static ImagesManager Images {get; private set;}
...
void Awake() {
Weather = GetComponent<WeatherManager>();
Images = GetComponent<ImagesManager>();
startSequence = new List<IGameManager>();
startSequence.Add(Weather);
startSequence.Add(Images);
StartCoroutine(StartupManagers());
}
...
与我们设置WeatherManager 的方式不同,ImagesManager中的GetWebImage()不会在启动时自动调用。相反,代码会等待,直到被调用;这将在下一个部分。
Unlike the way we set up WeatherManager, GetWebImage() in ImagesManager isn’t called automatically on startup. Instead, the code waits until it’s invoked; that’ll happen in the next section.
这您刚刚编写的ImagesManager在被调用之前不会执行任何操作,因此现在我们将创建一个将调用ImagesManager中的方法的广告牌对象。首先创建一个新的立方体,然后将其放置在场景的中间,位置为0、1.5 、 -5 ,比例为5、3、0.5 (见图10.6)。
The ImagesManager you just wrote doesn’t do anything until it’s called upon, so now we’ll create a billboard object that will call methods in ImagesManager. First create a new cube and then place it in the middle of the scene, at something like Position 0, 1.5, -5 and Scale 5, 3, 0.5 (see figure 10.6).
Figure 10.6 The billboard object, before and after displaying the downloaded image
你将创建一个设备,其操作方式与第 9 章中的变色监视器一样。复制DeviceOperator脚本并将其放在播放器上。您可能还记得,当按下 C 键时,该脚本将操作附近的设备。还要为广告牌设备创建一个名为WebLoadingBillboard的脚本,将该脚本放在广告牌对象上,然后输入此代码。
You’re going to create a device that operates just like the color-changing monitor in chapter 9. Copy the DeviceOperator script and put it on the player. As you may recall, that script will operate nearby devices when the C key is pressed. Also create a script for the billboard device called WebLoadingBillboard, put that script on the billboard object, and enter this code.
清单 10.15 WebLoadingBillboard设备脚本
Listing 10.15 WebLoadingBillboard device script
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 WebLoadingBillboard:MonoBehaviour {
公共无效操作(){
经理.图像.获取Web图像(OnWebImage); ❶
}
私有 void OnWebImage(Texture2D 图像){
GetComponent<Renderer>().material.mainTexture = image; ❷
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WebLoadingBillboard : MonoBehaviour {
public void Operate() {
Managers.Images.GetWebImage(OnWebImage); ❶
}
private void OnWebImage(Texture2D image) {
GetComponent<Renderer>().material.mainTexture = image; ❷
}
}
❶ Call the method in ImagesManager.
❷ The downloaded image is applied to the material in the callback.
此代码主要执行两项操作:在设备运行时调用ImagesManager.GetWebImage(),并从回调函数应用图像。纹理应用于材质,因此您可以更改广告牌上材质的纹理。图 10.6 显示了您玩游戏后广告牌的外观。
This code does two primary things: it calls ImagesManager.GetWebImage() when the device is operated, and it applies the image from the callback function. Textures are applied to materials, so you can change the texture in the material that’s on the billboard. Figure 10.6 shows what the billboard will look like after you play the game.
太棒了,下载的图片显示在广告牌上!但这段代码可以进一步优化,以适用于多个广告牌。让我们来解决这个优化问题下一个。
Great, the downloaded image is displayed on the billboard! But this code could be optimized further to work with multiple billboards. Let’s tackle that optimization next.
作为如第 10.3.1 节所述,ImagesManager尚未存储已下载的图像。这意味着图像将一遍又一遍地下载用于多个广告牌。这是低效的,因为每次都是相同的图像。为了解决这个问题,我们将调整ImagesManager以缓存已下载的图像。
As noted in section 10.3.1, ImagesManager doesn’t yet store the downloaded image. That means the image will be downloaded over and over for multiple billboards. This is inefficient, because it’ll be the same image each time. To address this, we’re going to adjust ImagesManager to cache images that have been downloaded.
定义 缓存表示保存在本地。最常见(但不是唯一!)的上下文涉及从互联网下载的文件,例如图像。
DEFINITION Cache means to keep stored locally. The most common (but not only!) context involves files, such as images, downloaded from the internet.
关键是在ImagesManager中提供一个回调函数,该函数首先保存图像,然后调用来自WebLoadingBillboard的回调。这很难做到(与直接使用来自WebLoadingBillboard的回调的当前代码相反),因为代码事先不知道来自WebLoadingBillboard的回调是什么。换句话说,没有办法在ImagesManager中编写一个方法来调用WebLoadingBillboard中的特定方法,因为我们还不知道那个特定方法是什么。解决这个难题的方法是使用 lambda 函数。
The key is to provide a callback function in ImagesManager that first saves the image, and then calls the callback from WebLoadingBillboard. This is tricky to do (as opposed to the current code that directly uses the callback from WebLoadingBillboard) because the code doesn’t know ahead of time what the callback from WebLoadingBillboard will be. Put another way, there’s no way to write a method in ImagesManager that calls a specific method in WebLoadingBillboard because we don’t yet know what that specific method will be. The way around this conundrum is to use lambda functions.
定义lambda函数(也称为匿名函数)是没有名称的函数。这些函数通常在其他函数内部动态创建。
DEFINITION A lambda function (also called an anonymous function) is a function that doesn’t have a name. These functions are usually created on the fly inside other functions.
Lambda 函数是多种编程语言(包括 C#)支持的一项棘手的代码功能。通过在ImagesManager中使用 lambda 函数进行回调,代码可以使用从WebLoadingBillboard传入的方法动态创建回调函数。您不需要提前知道要调用的方法,因为这个 lambda 函数事先并不存在!此清单显示了如何在ImagesManager中执行此巫术。
Lambda functions are a tricky code feature supported in multiple programming languages, including C#. By using a lambda function for the callback in ImagesManager, the code can create the callback function on the fly by using the method passed in from WebLoadingBillboard. You don’t need to know the method to call ahead of time, because this lambda function doesn’t exist ahead of time! This listing shows how to do this voodoo in ImagesManager.
清单 10.16 ImagesManager中的回调 Lambda 函数
Listing 10.16 Lambda function for callback in ImagesManager
使用系统;
...
公共无效GetWebImage(Action <Texture2D>回调){
如果 (webImage == null) {
StartCoroutine(网络。下载图像((Texture2D 图像)=> {
webImage = 图像; ❶
回调(webImage); ❷
}));
}
别的 {
回调(webImage);
}
}
...using System;
...
public void GetWebImage(Action<Texture2D> callback) {
if (webImage == null) {
StartCoroutine(network.DownloadImage((Texture2D image) => {
webImage = image; ❶
callback(webImage); ❷
}));
}
else {
callback(webImage);
}
}
...
❷在lambda函数中使用回调,而不是直接发送给NetworkService。
❷ The callback is used in the lambda function instead of being sent directly to NetworkService.
主要变化在于传递给NetworkService.DownloadImage() 的函数。以前,代码通过来自WebLoadingBillboard的相同回调方法传递。但是,更改后,发送给NetworkService 的回调是一个单独的 lambda 函数,该函数在现场声明,调用来自WebLoadingBillboard的方法。请注意声明 lambda 函数的语法:() => {}。
The main change is in the function passed to NetworkService.DownloadImage(). Previously, the code was passing through the same callback method from WebLoadingBillboard. After the change, though, the callback sent to NetworkService is a separate lambda function declared on the spot that called the method from WebLoadingBillboard. Take note of the syntax to declare a lambda function: () => {}.
将回调设为单独的函数可以实现比调用WebLoadingBillboard中的方法更多的功能;具体来说,lambda 函数还会存储已下载图像的本地副本。因此,GetWebImage()只需在第一次下载图像;所有后续调用都将使用本地存储的图像。
Making the callback a separate function makes it possible to do more than call the method in WebLoadingBillboard; specifically, the lambda function also stores a local copy of the downloaded image. Thus, GetWebImage() has to download the image only the first time; all subsequent calls will use the locally stored image.
由于此优化适用于后续调用,因此只有在多个广告牌上才会产生明显效果。让我们复制广告牌对象,以便场景中出现第二个广告牌。选择广告牌对象,单击“复制”(查看“编辑”菜单或右键单击),然后将复制的广告牌移过去(例如,将 X 位置更改为 18)。
Because this optimization applies to subsequent calls, the effect will be noticeable only on multiple billboards. Let’s duplicate the billboard object so that a second billboard will be in the scene. Select the billboard object, click Duplicate (look under the Edit menu or right-click), and move the duplicate over (for example, change the X position to 18).
现在玩游戏,看看会发生什么。当你操作第一个广告牌时,图像从互联网下载时会出现明显的停顿。但是当你走到第二个广告牌时,图像会立即出现,因为它已经被下载了。
Now play the game and watch what happens. When you operate the first billboard, a noticeable pause occurs while the image downloads from the internet. But when you then walk over to the second billboard, the image will appear immediately because it has already been downloaded.
这是下载图片的一个重要优化(网络浏览器默认缓存图片是有原因的)。还有一项重要的网络任务需要完成:将数据发送回这服务器。
This is an important optimization for downloading images (there’s a reason web browsers cache images by default). One more major networking task remains to go over: sending data back to the server.
我们我们已经看过了多个下载数据的示例,但我们仍然需要看一个发送数据的示例。最后一节确实要求您有一个服务器来发送请求,因此本节是可选的。但下载开源软件来设置服务器进行测试很容易。
We’ve gone over multiple examples of downloading data, but we still need to see an example of sending data. This last section does require you to have a server to send requests to, so this section is optional. But it’s easy to download open source software to set up a server to test on.
我建议使用 XAMPP 作为测试服务器。请访问www.apachefriends.org下载 XAMPP(在 macOS 上,您需要将 .bz2 重命名为 .dmg)并按照安装说明进行操作。安装完成后,服务器开始运行,您可以使用地址 http://localhost/ 访问 XAMPP 的 htdocs 文件夹,就像访问互联网上的服务器一样。启动并运行 XAMPP 后,在 htdocs 中创建一个名为uia的文件夹;您将在其中放置服务器端脚本。
I recommend XAMPP for a test server. Go to www.apachefriends.org to download XAMPP (on macOS you need to rename the .bz2 to .dmg) and follow the installation instructions. Once that’s installed and the server is running, you can access XAMPP’s htdocs folder with the address http://localhost/ just as you would a server on the internet. Once you have XAMPP up and running, create a folder called uia in htdocs; that’s where you’ll put the server-side script.
无论您使用 XAMPP 还是您自己现有的 Web 服务器,实际任务都是在玩家到达场景中的检查点时将天气数据发布到服务器。此检查点将是一个触发器体积,就像第 9 章中的门触发器一样。您需要创建一个新的立方体对象,将其放置在场景的一侧,将碰撞器设置为触发器,并应用半透明材质,就像您在第 9 章中所做的那样(记住,设置材质的渲染模式)。图 10.7 显示了应用了绿色半透明材质的检查点对象。
Whether you use XAMPP or your own existing web server, the actual task will be to post weather data to the server when the player reaches a checkpoint in the scene. This checkpoint will be a trigger volume, just like the door trigger in chapter 9. You need to create a new cube object, position it off to one side of the scene, set the collider to Trigger, and apply a semitransparent material as you did in chapter 9 (remember, set the material’s Rendering Mode). Figure 10.7 shows the checkpoint object with a green semitransparent material applied.
Figure 10.7 The checkpoint object that triggers data sending
Now that the trigger object is in the scene, let’s write the code that it invokes.
这检查点对象调用的代码将通过多个脚本级联。与下载数据的代码一样,发送数据的代码将涉及WeatherManager告诉NetworkService发出请求,而NetworkService处理 HTTP 通信的细节。这显示了您需要对NetworkService进行的调整。
The code that’s invoked by the checkpoint object will cascade through several scripts. As with the code for downloading data, the code for sending data will involve WeatherManager telling NetworkService to make the request, and NetworkService handles the details of HTTP communication. This shows the adjustments you need to make to NetworkService.
清单 10.17 调整NetworkService以发布数据
Listing 10.17 Adjusting NetworkService to post data
... 私有 const string localApi = "http://localhost/uia/api.php"; ❶ ... private IEnumerator CallAPI(string url, WWWForm form, Action<string> callAPI) { ❷ 使用(UnityWebRequest 请求 =(表单 == null)? UnityWebRequest.Get(url):UnityWebRequest.Post(url,form)){ ❸ 产生返回请求.SendWebRequest(); 如果(请求.结果==UnityWebRequest.结果.连接错误){ Debug.LogError($"网络问题:{request.error}"); } 否则,如果 (request.result == UnityWebRequest.Result.ProtocolError) { Debug.LogError($"响应错误:{request.responseCode}"); } 别的 { 回调(请求.downloadHandler.text); } } } 公共 IEnumerator GetWeatherXML(Action <string> 回调){ 返回 CallAPI(xmlApi, null,回调); ❹ } 公共 IEnumerator GetWeatherJSON(Action <string> 回调){ 返回 CallAPI(jsonApi,null,回调); } 公共 IEnumerator LogWeather(字符串名称,浮点云值,Action <string> 回调){ WWWForm 表单 = new WWWForm(); ❺ 表单.AddField("消息", 名称); 表单.AddField("云值",云值.ToString()); form.AddField("时间戳", DateTime.UtcNow.Ticks.ToString()); ❻ 返回 CallAPI(localApi, 表单, 回调); } ...
... private const string localApi = "http://localhost/uia/api.php"; ❶ ... private IEnumerator CallAPI(string url, WWWForm form, Action<string> callback) { ❷ using (UnityWebRequest request = (form == null) ? UnityWebRequest.Get(url) : UnityWebRequest.Post(url, form)) { ❸ yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.ConnectionError) { Debug.LogError($"network problem: {request.error}"); } else if (request.result == UnityWebRequest.Result.ProtocolError) { Debug.LogError($"response error: {request.responseCode}"); } else { callback(request.downloadHandler.text); } } } public IEnumerator GetWeatherXML(Action<string> callback) { return CallAPI(xmlApi, null, callback); ❹ } public IEnumerator GetWeatherJSON(Action<string> callback) { return CallAPI(jsonApi, null, callback); } public IEnumerator LogWeather(string name, float cloudValue, Action<string> callback) { WWWForm form = new WWWForm(); ❺ form.AddField("message", name); form.AddField("cloud_value", cloudValue.ToString()); form.AddField("timestamp", DateTime.UtcNow.Ticks.ToString()); ❻ return CallAPI(localApi, form, callback); } ...
❶ Address of the server-side script; change this if needed.
❷ Added arguments to CallAPI() parameters
❸ Either POST using WWWForm or GET without
❹ Calls modified because of changed parameters
❺ Define a form with values to send.
❻ Send a timestamp along with the cloudiness.
首先,请注意CallAPI()有一个新参数。这是一个WWWForm对象,一系列要随 HTTP 请求一起发送的值。代码中的条件使用WWWForm对象的存在来更改创建的请求。通常我们希望发送 GET 请求,但WWWForm会将其更改为 POST 请求以发送数据。代码中的所有其他更改都会对该核心更改做出反应(例如,由于CallAPI()参数而修改GetWeather()代码)。以下代码是您需要在WeatherManager中添加的内容。
First, notice that CallAPI() has a new parameter. This is a WWWForm object, a series of values to send along with the HTTP request. A condition in the code uses the presence of a WWWForm object to alter the request created. Normally we want to send a GET request, but WWWForm will change it to a POST request to send data. All the other changes in the code react to that central change (for example, modifying the GetWeather() code because of the CallAPI() parameters). The following code is what you need to add in WeatherManager.
清单 10.18 向WeatherManager添加发送数据的代码
Listing 10.18 Adding code to WeatherManager that sends data
...
公共无效LogWeather(字符串名称){
StartCoroutine(网络.LogWeather(名称,cloudValue,OnLogged));
}
私有 void OnLogged (字符串响应){
调试.日志(响应);
}
......
public void LogWeather(string name) {
StartCoroutine(network.LogWeather(name, cloudValue, OnLogged));
}
private void OnLogged(string response) {
Debug.Log(response);
}
...
最后,利用此代码向场景中的触发体积添加检查点脚本。创建一个名为CheckpointTrigger的脚本,将该脚本放在触发体积上,然后输入下一个清单的内容。
Finally, make use of this code by adding a checkpoint script to the trigger volume in the scene. Create a script called CheckpointTrigger, put that script on the trigger volume, and enter the contents of the next listing.
清单 10.19触发卷的CheckpointTrigger脚本
Listing 10.19 CheckpointTrigger script for the trigger volume
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 CheckpointTrigger:MonoBehaviour {
公共字符串标识符;
私有 bool 触发; ❶
void OnTriggerEnter(Collider 其他){
如果(触发){return;}
经理.Weather.LogWeather(标识符); ❷
触发=真;
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CheckpointTrigger : MonoBehaviour {
public string identifier;
private bool triggered; ❶
void OnTriggerEnter(Collider other) {
if (triggered) {return;}
Managers.Weather.LogWeather(identifier); ❷
triggered = true;
}
}
❶ Track if the checkpoint has already been triggered.
检查器中会出现一个标识符槽;将其命名为checkpoint1。运行代码,当你进入检查点时,数据就会被发送。但是,响应会显示错误,因为服务器上没有脚本来接收请求。这是此步骤的最后一步部分。
An Identifier slot will appear in the Inspector; name it something like checkpoint1. Run the code, and data will be sent when you enter the checkpoint. The response will indicate an error, though, because no script is on the server to receive the request. That’s the last step in this section.
这服务器需要有一个脚本来接收游戏发送的数据。编写服务器脚本超出了本书的范围,所以我们不会在这里详细介绍。我们只需编写一个 PHP 脚本,因为这是最简单的方法。在 htdocs(或您的 Web 服务器所在的任何地方)中创建一个文本文件并将其命名为api.php(清单 10.20)。
The server needs to have a script to receive data sent from the game. Coding server scripts is beyond the scope of this book, so we won’t go into detail here. We’ll just whip up a PHP script, because that’s the easiest approach. Create a text file in htdocs (or wherever your web server is located) and name it api.php (listing 10.20).
Listing 10.20 Server script written in PHP that receives our data
<?php $message = $_POST['message']; ❶ $cloudiness = $_POST['cloud_value']; $时间戳 = $_POST['时间戳']; $combined = $message。" cloudiness=".$cloudiness。" time=".$timestamp。"\n"; $filename = “data.txt”; ❷ file_put_contents($filename, $combined, FILE_APPEND | LOCK_EX); ❸ 回显“已记录”; >
<?php $message = $_POST['message']; ❶ $cloudiness = $_POST['cloud_value']; $timestamp = $_POST['timestamp']; $combined = $message." cloudiness=".$cloudiness." time=".$timestamp."\n"; $filename = "data.txt"; ❷ file_put_contents($filename, $combined, FILE_APPEND | LOCK_EX); ❸ echo "Logged"; ?>
❶ Extract post data into variables.
❷ Define the filename to write to.
请注意,此脚本将收到的数据写入 data.txt,因此您还需要在服务器上放置一个同名的文本文件。api.php 到位后,当触发检查点时,您将看到天气日志出现在 data.txt 中游戏。伟大的!
Note that this script writes received data into data.txt, so you also need to put a text file with that name on the server. Once api.php is in place, you’ll see weather logs appear in data.txt when triggering checkpoints in the game. Great!
虽然在视频游戏内容方面,图形最受关注,但音频也至关重要。大多数游戏都会播放背景音乐并具有音效。因此,Unity 具有音频功能,因此您可以将音效和音乐放入游戏中。Unity 可以导入和播放各种音频文件格式,调整音量,甚至可以处理从场景中的特定位置播放的声音。
Although graphics get most of the attention when it comes to content in video games, audio is crucial too. Most games play background music and have sound effects. Accordingly, Unity has audio functionality so that you can put sound effects and music into your games. Unity can import and play a variety of audio file formats, adjust the volume of sounds, and even handle sounds playing from a specific position within the scene.
注意2D 和 3D 游戏的音频处理方式相同。虽然本章中的示例项目是 3D 游戏,但我们所做的一切也适用于 2D 游戏。
NOTE Audio is handled the same way for both 2D and 3D games. Although the sample project in this chapter is a 3D game, everything we’ll do applies to 2D games as well.
本章首先讨论音效而不是音乐。音效是与游戏中的动作一起播放的短片段(例如玩家开火时播放的枪声),而音乐的声音片段较长(通常长达几分钟),并且播放与游戏中的事件没有直接关系。最终,两者都归结为同一种音频文件和播放代码,但音乐的声音文件通常比音效的短片段大得多(事实上,音乐文件通常是游戏中最大的文件!),这一简单事实值得在单独的章节中介绍它们。
This chapter starts off looking at sound effects rather than music. Sound effects are short clips that play along with actions in the game (such as a gunshot that plays when the player fires), whereas the sound clips for music are longer (often running into minutes) and playback isn’t directly tied to events in the game. Ultimately, both boil down to the same kind of audio files and playback code, but the simple fact that the sound files for music are usually much larger than the short clips used for sound effects (indeed, files for music are often the largest files in the game!) merits covering them in a separate section.
The complete road map for this chapter will be to take a game without sound and do the following:
注意在本章中,我们将在现有游戏演示的基础上添加音频功能。本章中的所有示例都是在第 3 章中创建的 FPS 的基础上构建的,您可以下载该示例项目,但您可以自由使用任何您想要的游戏演示。
NOTE In this chapter, we’ll simply add audio capabilities on top of an existing game demo. All of the examples in this chapter are built on top of the FPS created in chapter 3, and you could download that sample project, but you’re free to use whatever game demo you’d like.
一旦您复制了现有的游戏演示以供本章使用,您就可以着手第一步:导入音效。
Once you have an existing game demo copied to use for this chapter, you can tackle the first step: importing sound effects.
前如果你想播放任何声音,显然你需要将声音文件导入到你的 Unity 项目中。首先,你需要收集所需文件格式的声音片段,然后将文件导入 Unity 并根据你的目的进行调整。
Before you can play any sounds, you obviously need to import the sound files into your Unity project. First, you’ll collect sound clips in the desired file format, and then you’ll bring the files into Unity and adjust them for your purposes.
很多正如您在第 4 章中看到的艺术资产一样,Unity 支持各种音频格式,各有优缺点。表 11.1 列出了 Unity 支持的音频文件格式。
Much as you saw with art assets in chapter 4, Unity supports a variety of audio formats with different pros and cons. Table 11.1 lists the audio file formats that Unity supports.
区分音频文件的主要考虑因素是所应用的压缩。压缩可以减小文件大小,但会从文件中丢弃一些信息。音频压缩的巧妙之处在于只丢弃最不重要的信息,这样压缩后的声音听起来仍然很好听。
The primary consideration differentiating audio files is the compression applied. Compression reduces a file’s size but accomplishes that by throwing out a bit of information from the file. Audio compression is clever about throwing out only the least important information so that the compressed sound still sounds good.
Table 11.1 Audio file formats supported by Unity
尽管如此,压缩还是会导致少量的质量损失,因此当声音片段较短且文件不大时,您应该选择未压缩的音频。较长的声音片段(尤其是音乐)应该使用压缩音频,因为否则音频片段会过大。不过,Unity 为这一决定增加了一点小麻烦。
Nevertheless, compression results in a small amount of loss of quality, so you should choose uncompressed audio when the sound clip is short and thus wouldn’t be a large file. Longer sound clips (especially music) should use compressed audio, because the audio clip would be prohibitively large otherwise. Unity adds a small wrinkle to this decision, though.
提示虽然音乐在最终游戏中应该被压缩,但 Unity 可以在您导入文件后压缩音频。在 Unity 中开发游戏时,您通常希望使用未压缩的文件格式,即使音乐很长,也不要导入压缩音频。
TIP Although music should be compressed in the final game, Unity can compress the audio after you’ve imported the file. When developing a game in Unity, you usually want to use uncompressed file formats even for lengthy music, as opposed to importing compressed audio.
由于 Unity 会在导入音频后对其进行压缩,因此您应始终选择 WAV 或 AIF 文件格式。您可能需要针对较短的音效和较长的音乐调整不同的导入设置(特别是,告诉 Unity 何时应用压缩),但原始文件应始终未压缩。
Because Unity will compress the audio after it’s been imported, you should always choose either WAV or AIF file format. You’ll probably need to adjust the import settings differently for short sound effects and longer music (in particular, to tell Unity when to apply compression), but the original files should always be uncompressed.
有多种方法可以创建声音文件(附录 B 提到了 Audacity 等工具,它可以录制来自麦克风的声音),但为了我们的目的,我们将从众多免费声音网站之一下载声音。我们将使用从www.freesound.org下载的WAV 文件格式的剪辑。
There are various ways to create sound files (appendix B mentions tools like Audacity, which can record sounds from a microphone), but for our purposes we’ll download sounds from one of the many free sound websites. We’re going to use clips downloaded from www.freesound.org in WAV file format.
警告“免费”声音是根据各种许可方案提供的,因此请务必确保您有权以您想要的方式使用声音片段。例如,许多免费声音仅供非商业用途。
WARNING “Free” sounds are offered under a variety of licensing schemes, so always make sure that you’re allowed to use the sound clip in the way you intend. For example, many free sounds are for noncommercial use only.
示例项目使用以下公共领域的声音效果(当然,您可以选择下载自己的声音;查找侧面列出的 0 许可证):
The sample project uses the following public domain sound effects (of course, you can choose to download your own sounds; look for a 0 license listed on the side):
Once you have the sound files to use in your game, the next step is to import the sounds into Unity.
后收集一些音频文件后,您需要将它们导入 Unity。就像您在第 4 章中处理艺术资产一样,您必须将音频资产导入项目,然后才能在游戏中使用它们。
After gathering some audio files, you need to bring them into Unity. Just as you did with art assets in chapter 4, you have to import audio assets into the project before they can be used in the game.
导入文件的机制很简单,与其他资产相同:将文件从计算机上的位置拖到 Unity 中的项目视图(创建一个名为Sound FX的文件夹以将文件拖入其中)。嗯,这很容易!但就像其他资产一样,这些音频文件具有导入设置(如图 11.1 所示)可在 Inspector 中进行调整。
The mechanics of importing files are simple and are the same as with other assets: drag the files from their location on the computer to the Project view within Unity (create a folder called Sound FX to drag the files into). Well, that was easy! But just like other assets, these audio files have import settings (shown in figure 11.1) to adjust in the Inspector.
Figure 11.1 Import settings for audio files
不要选中“强制为单声道”选项。这指的是单声道与立体声。通常,声音以立体声录制,导致文件中有两个波形,一个用于左耳/扬声器,一个用于右耳/扬声器。为了节省文件大小,您可能需要将音频信息减半,以便将相同的波形发送到两个扬声器,而不是将单独的波形发送到左右扬声器。(仅在单声道打开时适用的“标准化”设置,在单声道关闭时会变灰。)
Leave the Force To Mono option unchecked. This refers to mono versus stereo sound. Often, sounds are recorded in stereo, resulting in two waveforms in the file, one for the left ear/speaker, and one for the right. To save on file size, you might want to halve the audio information so that the same waveform is sent to both speakers rather than separate waves sent to the left and right speakers. (A Normalize setting, which applies only when mono is on, is grayed out when mono is off.)
在“强制单声道”下方,您会看到“在后台加载”和“预加载音频数据”复选框。预加载设置与平衡播放性能和内存使用有关;预加载音频将在声音等待使用时消耗内存,但可以避免等待加载。因此,您不想预加载较长的音频片段,但可以将其打开以播放此类短音效。
Below Force To Mono, you’ll see check boxes for Load In Background and Preload Audio Data. The preload setting relates to balancing playback performance and memory usage; preloading audio will consume memory while the sound waits to be used but will avoid having to wait to load. Thus, you don’t want to preload long audio clips, but turn it on for short sound effects like this.
同时,在后台加载音频将允许程序在加载音频时继续运行;这通常对于较长的音乐片段来说是个好主意,这样程序就不会冻结。但这意味着音频不会立即开始播放。通常,对于较短的声音片段,您需要关闭此设置,以确保它们在播放之前完全加载。由于导入的片段是短音效,因此您应该取消选中“在后台加载”。
Meanwhile, loading audio in the background will allow the program to keep running while the audio is loading; this is generally a good idea for long music clips so that the program doesn’t freeze. But this means the audio won’t start playing right away. Usually you want to keep this setting off for short sound clips to ensure that they load completely before they play. Because the imported clips are short sound effects, you should leave Load In Background unchecked.
最后,最重要的设置是加载类型和压缩格式。压缩格式控制存储的音频数据的格式。如上一节所述,音乐应该压缩,因此在这种情况下选择 Vorbis(这是一种压缩音频格式的名称)。短声音片段不需要压缩,因此选择 PCM(脉冲编码调制)(原始采样声波的技术术语)用于这些剪辑。第三个设置 ADPCM 是 PCM 的变体,有时会产生稍好一些的音质。
Finally, the most important settings are Load Type and Compression Format. Compression Format controls the formatting of the audio data that’s stored. As discussed in the previous section, music should be compressed, so choose Vorbis (it’s the name of a compressed audio format) in that case. Short sound clips don’t need to be compressed, so choose PCM (pulse code modulation, the technical term for the raw, sampled sound wave) for these clips. The third setting, ADPCM, is a variation on PCM and occasionally results in slightly better sound quality.
加载类型控制计算机如何加载文件中的数据。由于计算机内存有限,而音频文件可能很大,有时您希望音频在流入内存时播放,这样计算机就无需加载整个文件。但是,像这样流式传输音频时需要一些计算开销,因此音频在先加载到内存时播放速度最快。即便如此,您也可以选择加载的音频数据是压缩格式还是解压缩以便更快地播放。由于这些声音片段很短,因此它们不需要流式传输,可以设置为“加载时解压缩”。
Load Type controls how the data from the file will be loaded by the computer. Because computers have limited memory and audio files can be large, sometimes you want the audio to play while it’s streaming into memory, saving the computer from needing to have the entire file loaded. But a bit of computing overhead is required when streaming audio like this, so audio plays fastest when it’s loaded into memory first. Even then, you can choose whether the loaded audio data will be in compressed form or will be decompressed for faster playback. Because these sound clips are short, they don’t need to stream and can be set to Decompress On Load.
最后一个选项是“采样率设置”;将其保留为“保留采样率”,这样 Unity 就不会更改导入文件中的样本。此时,音效已全部导入并准备就绪到使用。
The last option is Sample Rate Setting; leave this at Preserve Sample Rate so Unity won’t change the samples in the imported file. At this point, the sound effects are all imported and ready to use.
现在你已经将声音文件添加到项目中,你自然想要播放声音。触发音效的代码并不难理解,但 Unity 中的音频系统确实有多个必须协同工作的部分。
Now that you have sound files added to the project, you naturally want to play the sounds. The code for triggering sound effects isn’t terribly hard to understand, but the audio system in Unity does have multiple parts that must work in concert.
虽然您可能认为播放声音只是告诉 Unity 播放哪个剪辑,但事实上,您必须定义三个部分才能在 Unity 中播放声音:AudioClip、AudioSource和AudioListener。将声音系统分成多个组件的原因与 Unity 对 3D 声音的支持有关:不同的组件告诉 Unity 它用于操纵 3D 声音的位置信息。
Although you might expect playing a sound to be simply a matter of telling Unity which clip to play, it turns out that you must define three parts in order to play sounds in Unity: AudioClip, AudioSource, and AudioListener. The reason for breaking the sound system into multiple components has to do with Unity’s support for 3D sounds: the different components tell Unity positional information that it uses for manipulating 3D sounds.
打个比方,想象现实世界中的一个房间。房间里有一台立体声音响正在播放 CD。如果一个人走进房间,他会听得很清楚。当他离开房间时,他听得就不那么清楚了,最后完全听不到了。同样,如果我们在房间里移动立体声音响,他会听到音乐随着音响移动而改变音量。如图 11.2 所示,在这个比喻中,CD 是一个AudioClip,立体声音响是一个AudioSource,而这个人是AudioListener。
As an analogy, imagine a room in the real world. The room has a stereo playing a CD. If a man comes into the room, he hears it clearly. When he leaves the room, he hears it less clearly, and eventually not at all. Similarly, if we move the stereo around the room, he’ll hear the music changing volume as it moves. As figure 11.2 illustrates, in this analogy the CD is an AudioClip, the stereo is an AudioSource, and the man is the AudioListener.
Figure 11.2 The three things you control in Unity’s audio system
这三个部分中的第一部分是音频剪辑。这是我们在上一节中导入的声音文件。此原始波形数据是音频系统执行其他所有操作的基础,但音频剪辑本身不会执行任何操作。
The first of the three parts is an audio clip. This is the sound file that we imported in the preceding section. This raw waveform data is the foundation for everything else the audio system does, but audio clips don’t do anything by themselves.
下一种对象是音频源。这是播放音频片段的对象。这是音频系统实际执行操作的抽象,但它是一种有用的抽象,使 3D 声音更容易理解。从特定音频源播放的 3D 声音位于该音频源的位置;2D 声音也必须从音频源播放,但位置无关紧要。
The next kind of object is an audio source. This is the object that plays audio clips. This is an abstraction over what the audio system is actually doing, but it’s a useful abstraction that makes 3D sounds easier to understand. A 3D sound played from a specific audio source is located at the position of that audio source; 2D sounds must also be played from an audio source, but the location doesn’t matter.
Unity 音频系统中涉及的第三种对象是音频监听器。顾名思义,这是听到从音频源投射出的声音的对象。这是音频系统所做工作的另一个抽象(显然,实际的听众是游戏玩家!),但是——就像音频源的位置决定了声音投射的位置一样——音频听众的位置决定了声音听到的位置。
The third kind of object involved in Unity’s audio system is an audio listener. As the name indicates, this is the object that hears sounds projected from the audio sources. This is another abstraction on top of what the audio system is doing (obviously, the actual listener is the player of the game!), but—much as the position of the audio source gives the position that the sound is projected from—the position of the audio listener gives the position that the sound is heard from.
尽管音频剪辑和AudioSource组件必须分配一个AudioListener组件在您创建新场景时,默认相机上已经存在。通常,您希望 3D 声音对查看器。
Although both the audio clips and the AudioSource components have to be assigned, an AudioListener component is already on the default camera when you create a new scene. Typically, you want 3D sounds to react to the position of the viewer.
全部好了,现在让我们在 Unity 中设置第一个声音!音频剪辑已经导入,默认相机有一个AudioListener组件,所以我们只需要分配一个AudioSource组件。我们将在 Enemy 预制件(四处游荡的敌人角色)上放置噼啪作响的火焰声音。
All right, now let’s set our first sound in Unity! The audio clips were already imported, and the default camera has an AudioListener component, so we need to assign only an AudioSource component. We’re going to put a crackling fire sound on the Enemy prefab, the enemy character that wanders around.
注意:由于敌人的声音听起来像是着火了,因此您可能需要为其添加一个粒子系统,使其看起来像着火了。您可以通过将粒子对象制作成预制件,然后从“资源”菜单中选择“导出包”来复制第 4 章中创建的粒子系统。或者,您可以在此处重做第 4 章中的步骤(首先双击“敌人”预制件以将其打开进行编辑,而不是编辑场景),从头开始创建一个新的粒子对象。
NOTE Because the enemy will sound like it’s on fire, you might want to give it a particle system so that it looks like it’s on fire. You can copy over the particle system created in chapter 4 by making the particle object into a prefab and then choosing Export Package from the Asset menu. Alternatively, you could redo the steps from chapter 4 here (after first double-clicking the Enemy prefab to open it for editing, rather than editing the scene) to create a new particle object from scratch.
通常,您需要将预制件打开到场景中才能对其进行编辑,但只需将组件添加到对象上即可,而无需双击预制件将其打开。选择 Enemy 预制件,以便其属性显示在 Inspector 中。现在添加一个新组件:选择 Audio > Audio Source。AudioSource组件将出现在 Inspector 中。
Usually, you need to open a prefab into the scene to edit it, but just adding a component onto the object can be done without double-clicking the prefab to open it. Select the Enemy prefab so that its properties appear in the Inspector. Now add a new component: choose Audio > Audio Source. An AudioSource component will appear in the Inspector.
告诉音频源播放什么声音片段。将音频文件从“项目”视图拖到“检查器”中的“音频片段”插槽;我们将在此示例中使用“壁炉”音效(参见图 11.3)。
Tell the audio source what sound clip to play. Drag an audio file from the Project view up to the Audio Clip slot in the Inspector; we’re going to use the “fireplace” sound effect for this example (refer to figure 11.3).
Figure 11.3 Settings for the AudioSource component
在设置中向下跳一点,选择“唤醒时播放”和“循环播放”(当然,确保未选中“静音”)。“唤醒时播放”指示音频源在场景开始时立即开始播放(在下一节中,您将学习如何在场景运行时手动触发声音)。“循环播放”指示音频源继续连续播放,播放结束时重复音频片段。
Skip down a bit in the settings and select both Play On Awake and Loop (of course, make sure that Mute isn’t checked). Play On Awake tells the audio source to begin playing as soon as the scene starts (in the next section, you’ll learn how to trigger sounds manually while the scene is running). Loop tells the audio source to keep playing continuously, repeating the audio clip when playback is over.
您希望此音频源投射 3D 声音。如前所述,3D 声音在场景中具有独特的位置。音频源的这一方面是使用“空间混合”设置进行调整的,该设置是从 2D 到 3D 的滑块。将此音频源设置为 3D。
You want this audio source to project 3D sounds. As explained earlier, 3D sounds have a distinct position within the scene. That aspect of the audio source is adjusted using the Spatial Blend setting, which is a slider from 2D to 3D. Set it to 3D for this audio source.
现在玩游戏,确保扬声器已打开。你可以听到敌人发出噼啪作响的声音,如果你走开,声音就会变得微弱,因为你使用了 3D 音频来源。
Now play the game and make sure your speakers are turned on. You can hear a crackling fire coming from the enemy, and the sound becomes faint if you move away because you used a 3D audio source.
环境AudioSource组件自动播放对于某些循环声音来说很方便,但对于大多数音效,您需要使用代码命令触发声音。这种方法仍然需要AudioSource组件,但现在音频源只会在程序通知时播放声音片段,而不是一直自动播放。
Setting the AudioSource component to play automatically is handy for some looping sounds, but for the majority of sound effects, you’ll want to trigger the sound with code commands. That approach still requires an AudioSource component, but now the audio source will play sound clips only when told to by the program, instead of automatically all the time.
向播放器对象(而非相机对象)添加AudioSource组件。您不必链接到特定的音频剪辑,因为音频剪辑将在代码中定义。您可以关闭“唤醒时播放”,因为来自此源的声音将在代码中触发。此外,将“空间混合”调整为 3D,因为此声音位于场景中。现在,对处理射击的脚本RayShooter进行以下清单中所示的添加。
Add an AudioSource component to the player object (not the camera object). You don’t have to link in a specific audio clip because the audio clips will be defined in code. You can turn off Play On Awake because sounds from this source will be triggered in code. Also, adjust Spatial Blend to 3D because this sound is located in the scene. Now make the additions shown in the next listing to RayShooter, the script that handles shooting.
Listing 11.1 Sound effects added in the RayShooter script
... [序列化字段]音频源声音源; [SerializeField] AudioClip hitWallSound; ❶ [SerializeField] AudioClip hitEnemySound; ❶ ... 如果 (target != null) { ❷ 目标.ReactToHit(); soundSource.PlayOneShot(hitEnemySound); ❸ } 别的 { 启动协同程序(SphereIndicator(hit.point)); soundSource.PlayOneShot(hitWallSound); ❹ } ...
... [SerializeField] AudioSource soundSource; [SerializeField] AudioClip hitWallSound; ❶ [SerializeField] AudioClip hitEnemySound; ❶ ... if (target != null) { ❷ target.ReactToHit(); soundSource.PlayOneShot(hitEnemySound); ❸ } else { StartCoroutine(SphereIndicator(hit.point)); soundSource.PlayOneShot(hitWallSound); ❹ } ...
❶ References the two sound files you want to play
❷ If target is not null, the player has hit an enemy, so . . .
❸ ... 调用 PlayOneShot() 播放 Hit An Enemy 声音,或者 ...
❸ . . . call PlayOneShot() to play the Hit An Enemy sound, or . . .
❹ . . . 如果玩家没击中,则调用 PlayOneShot() 播放“Hit A Wall”声音。
❹ . . . call PlayOneShot() to play the Hit A Wall sound if the player missed.
新代码在脚本顶部包含几个序列化变量。将玩家对象(带有AudioSource组件的对象)拖到Inspector 中的soundSource插槽。然后将要播放的音频剪辑拖到声音插槽上;“swish”表示击中墙壁,“ding”表示击中敌人。
The new code includes several serialized variables at the top of the script. Drag the player object (the object with an AudioSource component) to the soundSource slot in the Inspector. Then drag the audio clips to play onto the sound slots; “swish” is for hitting the wall, and “ding” is for hitting the enemy.
另外添加的两行是PlayOneShot()方法。PlayOneShot ()使音频源播放给定的音频剪辑。在目标条件中添加这些方法,以便在击中各种物体时播放声音。
The other two lines added are PlayOneShot() methods. PlayOneShot() causes an audio source to play a given audio clip. Add those methods inside the target conditional to play sounds when various objects are hit.
注意:您可以在AudioSource中设置剪辑并调用Play()来播放剪辑。但是,多个声音会相互切断,因此我们改用PlayOneShot()。用此代码替换PlayOneShot()并快速拍摄一堆以查看(呃,听到)问题:soundSource.clip=hitEnemySound; soundSource.Play();。
NOTE You could set the clip in the AudioSource and call Play() to play the clip. Multiple sounds would cut one another off, though, so we used PlayOneShot() instead. Replace PlayOneShot() with this code and shoot a bunch rapidly to see (er, hear) the problem: soundSource.clip=hitEnemySound; soundSource.Play();.
好了,开始游戏,四处射击。现在游戏中有几种音效了。这些基本步骤也可用于添加各种音效。不过,游戏中强大的声音系统需要的远不止一堆不连贯的声音;至少,所有游戏都应该提供音量控制。接下来,您将通过中央声音的模块。
All right, play the game and shoot around. You now have several sound effects in the game. These same basic steps can be used to add all sorts of sound effects. A robust sound system in a game requires a lot more than a bunch of disconnected sounds, though; at a minimum, all games should offer volume control. You’ll implement that control next through a central audio module.
继续按照前面章节中建立的代码架构,你将创建一个AudioManager。回想一下,Managers对象包含游戏使用的各种代码模块的主列表,例如玩家库存管理器。这次,您将创建一个音频管理器以添加到列表中。这个中央音频模块将允许您调节游戏中的音频音量,甚至将其静音。最初,您只需要担心音效,但在后面的部分中,您将扩展 AudioManager以处理音乐。
Continuing the code architecture established in previous chapters, you’re going to create an AudioManager. Recall that the Managers object has a master list of the various code modules used by the game, such as a manager for the player’s inventory. This time, you’ll create an audio manager to stick into the list. This central audio module will allow you to modulate the volume of audio in the game and even mute it. Initially, you’ll worry about only sound effects, but in later sections you’ll extend the AudioManager to handle music as well.
这设置AudioManager 的第一步是建立管理器代码框架。从第 10 章项目中复制IGameManager、ManagerStatus和NetworkService;我们不会更改它们。(请记住,IGameManager是所有管理器都必须实现的接口,而ManagerStatus是IGameManager使用的枚举。NetworkService提供对互联网的调用,本章不会使用。)
The first step in setting up AudioManager is to put in place the Managers code framework. From the chapter 10 project, copy over IGameManager, ManagerStatus, and NetworkService; we won’t change them. (Remember that IGameManager is the interface that all managers must implement, whereas ManagerStatus is an enum that IGameManager uses. NetworkService provides calls to the internet and won’t be used in this chapter.)
注意Unity 可能会发出警告,因为NetworkService已分配但未使用。您可以忽略 Unity 的警告;我们希望启用代码框架来访问互联网,即使我们在本章中不使用该功能。
NOTE Unity will probably issue a warning because NetworkService is assigned but not used. You can ignore Unity’s warning; we want to enable the code framework to access the internet, even though we don’t use that functionality in this chapter.
还要复制 Managers 文件,该文件将针对新的AudioManager进行调整。暂时保留原样(或者如果编译器错误让您抓狂,请注释掉错误部分!)。创建一个名为AudioManager的新脚本,以供Managers代码引用。
Also copy over the Managers file, which will be adjusted for the new AudioManager. Leave it as is for now (or comment out the erroneous sections if the sight of compiler errors drives you crazy!). Create a new script called AudioManager that the Managers code can refer to.
Listing 11.2 Skeleton code for AudioManager
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 AudioManager : MonoBehaviour,IGameManager {
公共 ManagerStatus 状态 {获取;私人设置;}
私有网络服务网络;
// 在此处添加音量控制(清单 11.4)
公共无效启动(NetworkService服务){
Debug.Log("音频管理器正在启动...");
网络=服务;
// 在这里初始化音乐源(清单 11.11) ❶
状态 = ManagerStatus.Started; ❷
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AudioManager : MonoBehaviour, IGameManager {
public ManagerStatus status {get; private set;}
private NetworkService network;
// Add volume controls here (listing 11.4)
public void Startup(NetworkService service) {
Debug.Log("Audio manager starting...");
network = service;
// Initialize music sources here (listing 11.11) ❶
status = ManagerStatus.Started; ❷
}
}
❶ Any long-running startup tasks go here.
❷如果有长时间运行的启动任务,则将状态设置为“正在初始化”。
❷ Set status to Initializing if there are long-running startup tasks.
这个初始代码看起来像前几章中的管理器;这是IGameManager要求类实现的最小数量。管理器脚本现在可以与新经理进行调整。
This initial code looks like managers from previous chapters; this is the minimum amount that IGameManager requires the class to implement. The Managers script can now be adjusted with the new manager.
清单 11.3使用AudioManager调整的Managers脚本
Listing 11.3 Managers script adjusted with AudioManager
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
[RequireComponent(typeof(AudioManager))]
公共类管理器:MonoBehaviour {
公共静态 AudioManager 音频 {获取;私人设置;}
私有列表<IGameManager> startSequence;
无效唤醒(){
Audio = GetComponent<AudioManager>(); ❶
开始序列 = 新列表 <IGameManager>();
开始序列.添加(音频);
启动协同程序(StartupManagers());
}
私有 IEnumerator StartupManagers() {
NetworkService 网络 = 新的 NetworkService();
foreach(startSequence 中的 IGameManager 管理器){
管理器.启动(网络);
}
产量返回 null;
int numModules = 启动序列.计数;
int 数量就绪 = 0;
while (numReady < numModules) {
int lastReady = numReady;
准备数量=0;
foreach(startSequence 中的 IGameManager 管理器){
如果 (manager.status == ManagerStatus.Started) {
数量就绪++;
}
}
如果 (numReady > lastReady)
Debug.Log($"进度:{numReady}/{numModules}");
产量返回 null;
}
Debug.Log("所有管理器已启动");
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(AudioManager))]
public class Managers : MonoBehaviour {
public static AudioManager Audio {get; private set;}
private List<IGameManager> startSequence;
void Awake() {
Audio = GetComponent<AudioManager>(); ❶
startSequence = new List<IGameManager>();
startSequence.Add(Audio);
StartCoroutine(StartupManagers());
}
private IEnumerator StartupManagers() {
NetworkService network = new NetworkService();
foreach (IGameManager manager in startSequence) {
manager.Startup(network);
}
yield return null;
int numModules = startSequence.Count;
int numReady = 0;
while (numReady < numModules) {
int lastReady = numReady;
numReady = 0;
foreach (IGameManager manager in startSequence) {
if (manager.status == ManagerStatus.Started) {
numReady++;
}
}
if (numReady > lastReady)
Debug.Log($"Progress: {numReady}/{numModules}");
yield return null;
}
Debug.Log("All managers started up");
}
}
❶本项目中只列出AudioManager,而不列出PlayerManager等等。
❶ List only AudioManager in this project, instead of PlayerManager, and so on.
和前面的章节一样,在场景中创建 Game Managers 对象,然后将Managers和AudioManager附加到空对象。玩游戏时会在控制台中显示管理器的启动消息,但音频管理器不会执行任何操作然而。
As you have in previous chapters, create the Game Managers object in the scene and then attach both Managers and AudioManager to the empty object. Playing the game will show the managers’ startup messages in the console, but the audio manager doesn’t do anything yet.
和基本AudioManager设置完成后,是时候为其提供音量控制功能了。然后 UI 显示将使用这些音量控制方法来静音音效或调整音量。
With the bare-bones AudioManager set up, it’s time to give it volume control functionality. These volume control methods will then be used by UI displays to mute the sound effects or adjust the volume.
您将使用第 7 章重点介绍的 UI 工具。具体来说,您将创建一个带有按钮和滑块的弹出窗口来控制音量设置(见图 11.4)。我将列出所涉及的步骤,但不做详细介绍;如果您需要复习,请参阅第 7 章。如果需要,请在开始之前安装 TextMeshPro 和 2D Sprite 包(请参阅第 5 章和第 6 章):
You’ll use the UI tools that were the focus of chapter 7. Specifically, you’re going to create a pop-up window with a button and a slider to control volume settings (see figure 11.4). I’ll list the steps involved without going into detail; if you need a refresher, refer to chapter 7. If needed, install the TextMeshPro and 2D Sprite packages (refer back to chapters 5 and 6 for these) before starting:
In the Sprite Editor, set a 12-pixel border on all sides (remember to apply changes).
(Optional) Name the object HUD Canvas and switch to 2D view mode.
Create an image connected to that canvas (GameObject > UI > Image).
Set the slider’s Value (at the bottom of the Inspector) to 1.
Figure 11.4 UI display for mute and volume control
以上就是创建设置弹出窗口的所有步骤!现在已创建了弹出窗口,让我们编写与其配合使用的代码。这将涉及弹出窗口对象本身的脚本以及弹出窗口脚本调用的音量控制功能。首先,根据此清单调整AudioManager中的代码。
Those are all the steps to create the settings pop-up! Now that the pop-up has been created, let’s write code that it’ll work with. This will involve a script on the pop-up object itself as well as the volume control functionality that the pop-up script calls. First, adjust the code in AudioManager according to this listing.
Listing 11.4 Volume control added to AudioManager
...
公共浮点声音音量 { ❶
获取 {返回 AudioListener.volume;} ❷
设置 {AudioListener.volume = value;} ❷
}
公共 bool soundMute { ❸
获取 {return AudioListener.pause;}
设置 {AudioListener.pause = value;}
}
public void Startup(NetworkService service) { ❹
Debug.Log("音频管理器正在启动...");
网络=服务;
声音音量 = 1f; ❺
状态 = ManagerStatus.已启动;
}
......
public float soundVolume { ❶
get {return AudioListener.volume;} ❷
set {AudioListener.volume = value;} ❷
}
public bool soundMute { ❸
get {return AudioListener.pause;}
set {AudioListener.pause = value;}
}
public void Startup(NetworkService service) { ❹
Debug.Log("Audio manager starting...");
network = service;
soundVolume = 1f; ❺
status = ManagerStatus.Started;
}
...
❶ Property with getter and setter for volume
❷使用 AudioListener 实现 getter/setter。
❷ Implement the getter/setter using AudioListener.
❸ Add a similar property to mute.
❹ Italicized code was already in script, shown here for reference.
❺ Initialize the value (0 to 1 range; 1 is full volume).
已将soundVolume和soundMute属性添加到AudioManager。对于这两个属性,获取并设置函数是使用AudioListener上的全局值实现的。AudioListener类可以调节所有AudioListener实例接收到的所有声音的音量。设置AudioManager的soundVolume属性与在AudioListener上设置音量具有相同的效果。这里的优点是封装:与音频有关的所有事情都在单个管理器中处理,管理器外部的代码无需知道实现的细节。
Properties for soundVolume and soundMute were added to AudioManager. For both properties, the get and set functions were implemented using global values on AudioListener. The AudioListener class can modulate the volume of all sounds received by all AudioListener instances. Setting AudioManager’s soundVolume property has the same effect as setting the volume on AudioListener. The advantage here is encapsulation: everything having to do with audio is being handled in a single manager, without code outside the manager needing to know the details of the implementation.
将这些方法添加到AudioManager后,您现在可以为弹出窗口编写脚本。创建一个名为SettingsPopup的脚本并添加此列表的内容。
With those methods added to AudioManager, you can now write a script for the pop-up. Create a script called SettingsPopup and add the contents of this listing.
清单 11.5 SettingsPopup脚本,其中包含用于调整音量的控件
Listing 11.5 SettingsPopup script with controls for adjusting the volume
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 SettingsPopup:MonoBehaviour {
public void OnSoundToggle() { ❶
经理.音频.声音静音 = !经理.音频.声音静音;
}
公共无效OnSoundValue(float volume){ ❷
Managers.Audio.soundVolume = 音量;
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SettingsPopup : MonoBehaviour {
public void OnSoundToggle() { ❶
Managers.Audio.soundMute = !Managers.Audio.soundMute;
}
public void OnSoundValue(float volume) { ❷
Managers.Audio.soundVolume = volume;
}
}
❶ Button will toggle the mute property of AudioManager.
❷ Slider will adjust the volume property of AudioManager.
此脚本有两种方法可以影响AudioManager的属性:OnSoundToggle()设置soundMute属性,并且OnSoundValue()设置了soundVolume属性。像往常一样,通过将SettingsPopup脚本拖到UI 中的Settings Popup对象上来链接它。
This script has two methods that affect the properties of AudioManager: OnSoundToggle() sets the soundMute property, and OnSoundValue() sets the soundVolume property. As usual, link in the SettingsPopup script by dragging it onto the Settings Popup object in the UI.
然后,要从按钮和滑块调用函数,请将弹出对象链接到这些控件中的交互事件。在按钮的检查器中,查找标有 On Click 的面板。单击 + 按钮以向此事件添加新条目。将Settings Popup拖到新条目中的对象槽,然后在菜单中查找SettingsPopup ;选择OnSoundToggle()以使按钮调用该函数。
Then, to call the functions from the button and slider, link the pop-up object to interaction events in those controls. In the Inspector for the button, look for the panel labeled On Click. Click the + button to add a new entry to this event. Drag Settings Popup to the object slot in the new entry and then look for SettingsPopup in the menu; select OnSoundToggle() to make the button call that function.
现在选择滑块并链接一个函数,就像您对按钮所做的那样。首先在滑块设置的面板中查找交互事件;在本例中,该面板称为 OnValueChanged。单击 + 按钮添加新条目,然后将Settings Popup拖到对象槽中。在函数菜单中,找到SettingsPopup脚本然后选择动态浮点下的OnSoundValue() 。
Now select the slider and link a function, just as you did with the button. First look for the interaction event in a panel of the slider’s settings; in this case, the panel is called OnValueChanged. Click the + button to add a new entry and then drag Settings Popup to the object slot. In the function menu, find the SettingsPopup script and then choose OnSoundValue() under Dynamic Float.
警告请记住选择动态浮点下的函数而不是静态参数!虽然该方法出现在列表的两个部分,但在后一种情况下,它将只接收提前输入的单个值。
WARNING Remember to choose the function under Dynamic Float and not Static Parameter! Although the method appears in both sections of the list, in the latter case it will receive only a single value typed in ahead of time.
设置控件现在可以正常工作了,但我们需要再处理一个脚本 — 弹出窗口目前总是遮住屏幕。一个简单的解决方法是让弹出窗口仅在按下 M 键时打开。创建一个名为UIController的新脚本,将该脚本链接到场景中的控制器对象,然后编写此代码。
The settings controls are now working, but we need to address one more script—the pop-up is currently always covering up the screen. A simple fix is to make the pop-up open only when you press the M key. Create a new script called UIController, link that script to the controller object in the scene, and write this code.
Listing 11.6 UIController that toggles the settings pop-up
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 UIController : MonoBehaviour {
[SerializeField] SettingsPopup 弹出; ❶
无效开始(){
popup.gameObject.SetActive(false); ❷
}
无效更新(){
if (Input.GetKeyDown(KeyCode.M)) { ❸
bool isShowing = popup.gameObject.activeSelf;
弹出.gameObject.SetActive(!正在显示);
如果(正在显示){
Cursor.lockState = CursorLockMode.Locked; ❹
Cursor.visible = false; ❹
} else { ❹
Cursor.lockState = CursorLockMode.None; ❹
Cursor.visible = true; ❹
}
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UIController : MonoBehaviour {
[SerializeField] SettingsPopup popup; ❶
void Start() {
popup.gameObject.SetActive(false); ❷
}
void Update() {
if (Input.GetKeyDown(KeyCode.M)) { ❸
bool isShowing = popup.gameObject.activeSelf;
popup.gameObject.SetActive(!isShowing);
if (isShowing) {
Cursor.lockState = CursorLockMode.Locked; ❹
Cursor.visible = false; ❹
} else { ❹
Cursor.lockState = CursorLockMode.None; ❹
Cursor.visible = true; ❹
}
}
}
}
❶ References the pop-up object in the scene
❷ Initializes the hidden pop-up
❸ Toggles the pop-up with the M key
❹ Also toggles the cursor along with the pop-up
要连接此对象引用,请将设置弹出窗口拖到此脚本上的插槽。立即播放并尝试更改滑块(请记住按 M 键激活 UI),同时四处射击以听取音效;您将听到音效根据滑块。
To wire up this object reference, drag the settings pop-up to the slot on this script. Play now and try changing the slider (remember to activate the UI by pressing the M key) while shooting around to hear the sound effects; you’ll hear the sound effects change volume according to the slider.
你现在要对AudioManager进行另一项添加,以允许 UI 在单击按钮时播放声音。由于 Unity 需要AudioSource,因此这项任务比乍一看要复杂得多。当场景中的对象发出声音效果时,将AudioSource附加到哪里是相当明显的。但 UI 声音效果不是场景的一部分,因此您将为 AudioManager 设置一个特殊的AudioSource ,以便在没有其他音频源时使用。
You’re going to make another addition to AudioManager now to allow the UI to play sounds when buttons are clicked. This task is more involved than it seems at first, owing to Unity’s need for an AudioSource. When sound effects were issued from objects in the scene, it was fairly obvious where to attach the AudioSource. But UI sound effects aren’t part of the scene, so you’ll set up a special AudioSource for AudioManager to use when there isn’t any other audio source.
创建一个新的空GameObject并将其附加为主 Game Managers 对象的子对象;这个新对象将有一个AudioManager使用的AudioSource,因此将新对象命名为Audio。向此对象添加一个AudioSource组件(此时将 Spatial Blend 设置保留为 2D,因为 UI 在场景中没有特定位置),然后添加此代码以在AudioManager中使用此源。
Create a new empty GameObject and attach it as a child of the main Game Managers object; this new object is going to have an AudioSource used by AudioManager, so call the new object Audio. Add an AudioSource component to this object (leave the Spatial Blend setting at 2D this time, because the UI doesn’t have a specific position in the scene) and then add this code to use this source in AudioManager.
Listing 11.7 Playing sound effects in AudioManager
... [SerializeField] AudioSource soundSource; ❶ ... 公共无效PlaySound(AudioClip clip){ ❷ 声音源.播放一张照片(剪辑); } ...
... [SerializeField] AudioSource soundSource; ❶ ... public void PlaySound(AudioClip clip) { ❷ soundSource.PlayOneShot(clip); } ...
❶ Variable slot in the Inspector to reference the new audio source
❷ Play sounds that don’t have any other source.
管理器的检查器中将出现一个新的变量槽;将音频对象拖到到这个插槽上。现在修改弹出脚本(如下面的清单所示)以添加 UI 音效。
A new variable slot will appear in the manager’s Inspector; drag the Audio object onto this slot. Now modify the pop-up script (as shown in the following listing) to add the UI sound effect.
Listing 11.8 Adding sound effects to SettingsPopup
... [SerializeField] AudioClip 声音; ❶ ... 公共无效OnSoundToggle(){ 经理.音频.声音静音 = !经理.音频.声音静音; 经理.Audio.播放声音(声音); ❷ } ...
... [SerializeField] AudioClip sound; ❶ ... public void OnSoundToggle() { Managers.Audio.soundMute = !Managers.Audio.soundMute; Managers.Audio.PlaySound(sound); ❷ } ...
❶ Inspector slot to reference the sound clip
❷ Play the sound effect when the button is clicked.
将 UI 音效拖到变量槽上;我使用了 2D 音效“thump”。单击 UI 按钮时,会同时播放该音效(当然,声音未静音时!)。尽管 UI 本身没有音频源,但AudioManager有一个播放音效的音频源。
Drag the UI sound effect onto the variable slot; I used the 2D sound “thump.” When you click the UI button, that sound effect plays at the same time (when the sound isn’t muted, of course!). Even though the UI doesn’t have an audio source itself, AudioManager has an audio source that plays the sound effect.
太棒了,我们已经设置好了所有音效!现在让我们把注意力转向到音乐。
Great, we’ve set up all our sound effects! Now let’s turn our attention to music.
你要将背景音乐添加到游戏中,您可以通过将音乐添加到AudioManager来实现。如章节介绍中所述,音乐片段与音效并没有根本区别。数字音频通过波形发挥作用的方式是相同的,播放音频的命令也大致相同。主要区别在于音频的长度,但这种差异会级联成许多后果。
You’re going to add background music to the game, and you’ll do that by adding music to AudioManager. As explained in the chapter introduction, music clips aren’t fundamentally different from sound effects. The way digital audio functions through waveforms is the same, and the commands for playing the audio are largely the same. The main difference is the length of the audio, but that difference cascades out into numerous consequences.
首先,音乐曲目往往会占用大量计算机内存,因此必须优化内存占用。您必须注意两个内存问题:在需要之前将音乐加载到内存中,以及加载时占用过多内存。
For starters, music tracks tend to consume a large amount of memory on the computer, and that memory consumption must be optimized. You must watch out for two areas of memory issues: having the music loaded into memory before it’s needed, and consuming too much memory when loaded.
使用Resources.Load()命令优化音乐加载时间在第 9 章中介绍过。正如您所了解的,此命令允许您按名称加载资源。虽然这肯定是一个方便的功能,但这并不是从 Resources 文件夹加载资源的唯一原因。另一个关键考虑因素是延迟加载:通常,Unity 会在场景加载后立即加载场景中的所有资源,但 Resources 中的资源直到代码手动获取它们时才会加载。在这种情况下,我们希望延迟加载音乐的音频剪辑。否则,即使没有使用音乐,音乐也会消耗大量内存。
Optimizing when music loads is done using the Resources.Load() command introduced in chapter 9. As you learned, this command allows you to load assets by name. Though that’s certainly one handy feature, that’s not the only reason to load assets from the Resources folder. Another key consideration is delaying loading: normally, Unity loads all assets in a scene as soon as the scene loads, but assets from Resources aren’t loaded until the code manually fetches them. In this case, we want to lazy-load the audio clips for music. Otherwise, the music could consume a lot of memory even when it isn’t being used.
定义使用延迟加载,文件不会提前加载,而是延迟加载直到需要时才加载。通常,如果在使用前提前加载数据,响应速度会更快(例如,声音会立即播放),但当响应速度不那么重要时,延迟加载可以节省大量内存。
DEFINITION With lazy loading, a file isn’t loaded ahead of time but rather is delayed until it’s needed. Typically, data responds faster (for example, the sound plays immediately) if it’s loaded in advance of use, but lazy loading can save a lot of memory when responsiveness doesn’t matter as much.
第二个内存考虑因素是通过从光盘流式传输音乐来解决的。如第 11.1.2 节所述,流式传输音频可使计算机无需一次加载整个文件。加载样式是导入的音频剪辑的检查器中的设置。最终,播放背景音乐需要几个步骤,包括涵盖这些内存优化的步骤。
The second memory consideration is dealt with by streaming music off the disc. As explained in section 11.1.2, streaming the audio saves the computer from ever needing to have the entire file loaded at once. The style of loading was a setting in the Inspector of the imported audio clip. Ultimately, playing background music requires several steps, including steps to cover these memory optimizations.
这播放音乐的过程涉及与 UI 音效相同的一系列步骤(背景音乐也是 2D 声音,场景中没有源),因此我们将再次经历所有这些步骤:
The process of playing music involves the same series of steps as UI sound effects did (background music is also 2D sound without a source within the scene), so we’re going to go through all those steps again:
每个步骤都会略作修改,以便与音乐(而不是音效)配合使用。让我们看看第一步。
Each step will be modified slightly to work with music instead of sound effects. Let’s look at the first step.
获得通过下载或录制曲目来制作一些音乐。对于示例项目,我访问了www.freesound.org并下载了以下公共域音乐循环:
Obtain some music by downloading or recording tracks. For the sample project, I went to www.freesound.org and downloaded the following public domain music loops:
将文件拖到 Unity 中导入,然后在 Inspector 中调整导入设置。如前所述,音乐的音频剪辑通常与音效的音频剪辑具有不同的设置。首先,音频格式应设置为 Vorbis,用于压缩音频。请记住,压缩音频的文件大小会小得多。压缩也会略微降低音频质量,但对于较长的音乐剪辑来说,这种轻微的降低是可以接受的;在出现的滑块中将质量设置为 50%。
Drag the files into Unity to import them and then adjust their import settings in the Inspector. As explained earlier, audio clips for music generally have different settings than audio clips for sound effects. First, the audio format should be set to Vorbis, for compressed audio. Remember, compressed audio will have a significantly smaller file size. Compression also degrades the audio quality slightly, but that slight degradation is an acceptable trade-off for long music clips; set Quality to 50% in the slider that appears.
下一个要调整的导入设置是加载类型。同样,音乐应该从光盘流式传输,而不是完全加载。从加载类型菜单中选择流式传输。同样,打开后台加载,这样游戏就不会在加载音乐时暂停或减速。
The next import setting to adjust is Load Type. Again, music should stream from the disc rather than being loaded completely. Choose Streaming from the Load Type menu. Similarly, turn on Load In Background so that the game won’t pause or slow down while music is loading.
即使调整了所有导入设置,资产文件也必须移动到正确的位置才能正确加载。请记住,Resources.Load()命令要求资产位于 Resources 文件夹中。创建一个名为Resources的新文件夹,在该文件夹中创建一个名为Music 的文件夹,然后将音频文件拖到 Music 文件夹中(参见图 11.5)。这样就搞定了步骤1.
Even after you adjust all the import settings, the asset files must be moved to the correct location in order to load correctly. Remember that the Resources.Load() command requires the assets to be in the Resources folder. Create a new folder called Resources, create a folder within that called Music, and drag the audio files into the Music folder (see figure 11.5). That takes care of step 1.
图 11.5 放置在 Resources 文件夹中的音乐音频片段
Figure 11.5 Music audio clips placed inside the Resources folder
步骤 2:设置 AudioSource 供 AudioManager 使用
Step 2: Setting up an AudioSource for AudioManager to use
第 2 步是创建一个新的AudioSource用于播放音乐。创建另一个空的GameObject,将此对象命名为Music 1(而不是Music,因为我们将在本章后面添加Music 2 ),并将其附加为 Audio 对象的子对象。
Step 2 is to create a new AudioSource for music playback. Create another empty GameObject, name this object Music 1 (instead of Music because we’ll add Music 2 later in the chapter), and attach it as a child of the Audio object.
将AudioSource组件添加到Music 1,然后调整组件中的设置。取消选择“唤醒时播放”,但这次打开“循环”选项;虽然音效通常只播放一次,但音乐会循环播放。将“空间混合”设置保留为 2D,因为音乐在场景中没有任何特定位置。
Add an AudioSource component to Music 1 and then adjust the settings in the component. Deselect Play On Awake but turn on the Loop option this time; whereas sound effects usually play only once, music plays over and over in a loop. Leave the Spatial Blend setting at 2D, because music doesn’t have any specific position in the scene.
您可能还想降低优先级值。对于音效,此值无关紧要,因此我们将该值保留为默认值 128。但对于音乐,您可能想降低此值,因此我将音乐源设置为60。此值告诉 Unity 在分层多个声音时哪些声音最重要;有点违反直觉的是,值越低,优先级越高。当同时播放太多声音时,音频系统将开始丢弃声音;通过使音乐的优先级高于音效,您可以确保当太多音效同时触发时,音乐会继续播放时间。
You may want to reduce the Priority value too. For sound effects, this value didn’t matter, so we left the value at the default 128. But for music, you probably want to lower this value, so I set the music source to 60. This value tells Unity which sounds are most important when layering multiple sounds; somewhat counterintuitively, lower values are higher priority. When too many sounds are playing simultaneously, the audio system will start discarding sounds; by making music higher priority than sound effects, you ensure that the music will keep playing when too many sound effects trigger at the same time.
步骤 3:编写代码以在 AudioManager 中播放音频片段
Step 3: Writing code to play the audio clips in AudioManager
这音乐音频源已设置,因此将以下清单添加到AudioManager。
The Music audio source has been set up, so add the following listing to AudioManager.
Listing 11.9 Playing music in AudioManager
... [序列化字段]音频源音乐1源; [SerializeField] string introBGMusic; ❶ [SerializeField] string levelBGMusic; ❶ ... 公共无效PlayIntroMusic(){ ❷ 播放音乐(Resources.Load($"音乐/{introBGMusic}")作为AudioClip); } 公共无效PlayLevelMusic(){ ❸ 播放音乐(Resources.Load($"音乐/{levelBGMusic}"作为AudioClip); } 私有 void PlayMusic(AudioClip clip) { ❹ 音乐1源.剪辑 = 剪辑; 音乐1源.播放(); } 公共无效停止音乐(){ 音乐1源.停止(); } ...
... [SerializeField] AudioSource music1Source; [SerializeField] string introBGMusic; ❶ [SerializeField] string levelBGMusic; ❶ ... public void PlayIntroMusic() { ❷ PlayMusic(Resources.Load($"Music/{introBGMusic}") as AudioClip); } public void PlayLevelMusic() { ❸ PlayMusic(Resources.Load($"Music/{levelBGMusic}") as AudioClip); } private void PlayMusic(AudioClip clip) { ❹ music1Source.clip = clip; music1Source.Play(); } public void StopMusic() { music1Source.Stop(); } ...
❶ Write music names in these strings.
❷ Load intro music from Resources.
❸ Load main music from Resources.
❹ Play music by setting AudioSource.clip.
与往常一样,当您选择 Game Managers 对象时,新的序列化变量将在 Inspector 中可见。将Music 1拖入音频源插槽。然后在两个字符串变量中输入音乐文件的名称:intro-synth和loop。
As usual, the new serialized variables will be visible in the Inspector when you select the Game Managers object. Drag Music 1 into the audio source slot. Then type in the names of the music files in the two string variables: intro-synth and loop.
添加的代码的其余部分调用用于加载和播放音乐的命令(或者,在最后添加的方法中,停止音乐)。Resources.Load()命令从 Resources 文件夹加载命名资产(考虑到文件放在 Resources 中的 Music 子文件夹中)。该命令返回一个通用对象,但可以使用as关键字将该对象转换为更具体的类型(在本例中为AudioClip )。
The remainder of the added code calls commands for loading and playing music (or, in the last added method, stopping the music). The Resources.Load() command loads the named asset from the Resources folder (taking into account that the files are placed in the Music subfolder within Resources). A generic object is returned by that command, but the object can be converted to a more specific type (in this case, an AudioClip) by using the as keyword.
然后将加载的音频剪辑传递到PlayMusic()方法中。此函数在AudioSource中设置剪辑,然后调用Play() 。正如我之前所解释的那样,使用PlayOneShot()可以更好地实现音效,但在AudioSource中设置剪辑是一种更强大的音乐方法,允许您停止或暂停播放音乐。
The loaded audio clip is then passed into the PlayMusic() method. This function sets the clip in the AudioSource and then calls Play(). As I explained earlier, sound effects are better implemented using PlayOneShot(), but setting the clip in the AudioSource is a more robust approach for music, allowing you to stop or pause the playing music.
Step 4: Adding music controls to the UI
这AudioManager中的新音乐播放方法除非从其他地方调用,否则不会执行任何操作。让我们向音频 UI 添加更多按钮,这些按钮在单击时会播放不同的音乐。以下是步骤,仅作简单说明(如果需要,请参阅第 7 章):
The new music playback methods in AudioManager won’t do anything unless they’re called from elsewhere. Let’s add more buttons to the audio UI that will play different music when clicked. Here are the steps again, enumerated with little explanation (refer to chapter 7 if needed):
Expand the button’s hierarchy to select the text label and set that to Level Music.
Repeat these steps twice more to create two additional buttons.
Position one at -105, -20 and the other at 105, -20 (so they appear on either side).
Change the first text label to Intro Music and the last text label to No Music.
现在弹出窗口有三个按钮用于播放不同的音乐。在SettingsPopup中编写一个链接到每个按钮的方法。
Now the pop-up has three buttons for playing different music. Write a method in SettingsPopup that will be linked to each button.
Listing 11.10 Adding music controls to SettingsPopup
...
公共无效OnPlayMusic(int选择器){ ❶
管理人员.音频.播放声音(声音);
开关(选择器){ ❷
情况 1:
管理人员.音频.播放音乐();
休息;
情况 2:
管理器.音频.播放级别音乐();
休息;
默认:
管理器.音频.停止音乐();
休息;
}
}
......
public void OnPlayMusic(int selector) { ❶
Managers.Audio.PlaySound(sound);
switch (selector) { ❷
case 1:
Managers.Audio.PlayIntroMusic();
break;
case 2:
Managers.Audio.PlayLevelMusic();
break;
default:
Managers.Audio.StopMusic();
break;
}
}
...
❶ This method gets a number parameter from the button.
❷ Call a different music function in AudioManager for each button.
请注意,该函数采用int参数这次;通常,按钮方法没有参数,只是由按钮触发。在这种情况下,我们需要区分这三个按钮,因此每个按钮都会发送不同的数字。
Note that the function takes an int parameter this time; normally, button methods don’t have a parameter and are simply triggered by the button. In this case, we need to distinguish between the three buttons, so each button will send a different number.
按照典型步骤将按钮连接到此代码:在 Inspector 中的 On Click 面板中添加一个条目,将弹出窗口拖到对象槽,然后从菜单中选择适当的功能。这次,会显示一个用于输入数字的文本框,因为OnPlayMusic ()接受数字作为参数。输入1表示 Intro Music,输入2表示 Level Music,输入其他任何数字表示 No Music(我选择了0)。switch语句在OnMusic()中播放介绍音乐或关卡音乐,具体取决于数字,如果数字不是 1 或 2,则默认停止音乐。
Go through the typical steps to connect a button to this code: add an entry to the On Click panel in the Inspector, drag the pop-up to the object slot, and choose the appropriate function from the menu. This time, a text box for typing in a number is displayed, because OnPlayMusic() takes a number for a parameter. Type 1 for Intro Music, 2 for Level Music, and anything else for No Music (I went with 0). The switch statement in OnMusic() plays intro music or level music, depending on the number, or stops the music as a default if the number isn’t 1 or 2.
在游戏过程中单击音乐按钮时,您会听到音乐。太棒了!代码正在从 Resources 文件夹加载音频剪辑。音乐播放效率很高,但我们仍需要添加两个改进点:单独的音乐音量控制和切换时的淡入淡出这音乐。
When you click the music buttons while the game is playing, you’ll hear the music. Great! The code is loading the audio clips from the Resources folder. Music plays efficiently, although we still have two bits of polish to add: separate music volume control and cross-fading when changing the music.
这游戏已经具有音量控制,目前它也影响音乐。不过,大多数游戏都有单独的音效和音乐音量控制,所以现在让我们解决这个问题。
The game already has volume control, and currently that affects the music too. Most games have separate volume controls for sound effects and music, though, so let’s tackle that now.
第一步是告诉音乐AudioSource忽略AudioListener上的设置。我们希望全局AudioListener上的音量和静音继续影响所有音效,但我们不希望此音量应用于音乐。清单 11.10 包含代码来告诉音乐源忽略AudioListener上的音量。以下清单还为音乐添加了音量控制和静音,因此将其添加到AudioManager中。
The first step is to tell the music AudioSource to ignore the settings on Audio- Listener. We want volume and mute on the global AudioListener to continue to affect all sound effects, but we don’t want this volume to apply to music. Listing 11.10 includes code to tell the music source to ignore the volume on AudioListener. The following listing also adds volume control and mute for music, so add it to AudioManager.
清单 11.11 在AudioManager中单独控制音乐音量
Listing 11.11 Controlling music volume separately in AudioManager
... 私有浮点数 _musicVolume; ❶ 公共浮动音乐音量{ 得到 { 返回_musicVolume; } 放 { _musicVolume = 值; 如果 (music1Source != null) { ❷ music1Source.volume = _musicVolume; } } } ... 公共 bool musicMute { 得到 { 如果 (music1Source != null) { 返回音乐1源.静音; } 返回 false; ❸ } 放 { 如果 (music1Source != null) { 音乐1源.静音 = 值; } } } public void Startup(NetworkService service) { ❹ Debug.Log("音频管理器正在启动..."); 网络=服务; music1Source.ignoreListenerVolume = true; ❺ music1Source.ignoreListenerPause = true; ❺ 声音音量 = 1f; 音乐音量=1f; 状态 = ManagerStatus.Started; ❹ } ❹ ...
... private float _musicVolume; ❶ public float musicVolume { get { return _musicVolume; } set { _musicVolume = value; if (music1Source != null) { ❷ music1Source.volume = _musicVolume; } } } ... public bool musicMute { get { if (music1Source != null) { return music1Source.mute; } return false; ❸ } set { if (music1Source != null) { music1Source.mute = value; } } } public void Startup(NetworkService service) { ❹ Debug.Log("Audio manager starting..."); network = service; music1Source.ignoreListenerVolume = true; ❺ music1Source.ignoreListenerPause = true; ❺ soundVolume = 1f; musicVolume = 1f; status = ManagerStatus.Started; ❹ } ❹ ...
❶私有变量不能直接访问,只能通过属性的 getter 来访问
❶ Private variable that won’t be accessed directly, only through the property’s getter
❷ Adjust volume of the AudioSource directly.
❸ Default value in case the AudioSource is missing
❹ Italicized code was already in script, shown here for reference.
❺这些属性告诉 AudioSource 忽略 AudioListener 音量。
❺ These properties tell the AudioSource to ignore the AudioListener volume.
此代码的关键在于意识到您可以直接调整AudioSource的音量,即使该音频源忽略了AudioListener中定义的全局音量。 音量和静音属性均可操纵单个音乐源。
The key to this code is realizing you can adjust the volume of an AudioSource directly, even though that audio source is ignoring the global volume defined in AudioListener. Properties for both volume and mute manipulate the individual music source.
Startup ()方法初始化音乐源,同时启用ignoreListenerVolume和ignoreListenerPause。顾名思义,这些属性会导致音频源忽略AudioListener上的全局音量设置。
The Startup() method initializes the music source with both ignoreListenerVolume and ignoreListenerPause turned on. As the names suggest, those properties cause the audio source to ignore the global volume setting on AudioListener.
现在您可以点击“播放”来验证音乐不再受现有音量控制的影响。让我们为音乐音量添加第二个 UI 控件;首先调整SettingsPopup。
You can click Play now to verify that the music is no longer affected by the existing volume control. Let’s add a second UI control for the music volume; start by adjusting SettingsPopup.
清单 11.12 SettingsPopup中的音乐音量控制
Listing 11.12 Music volume controls in SettingsPopup
...
公共无效OnMusicToggle(){
经理们.Audio.musicMute = !经理们.Audio.musicMute; ❶
管理人员.音频.播放声音(声音);
}
公共无效OnMusicValue(浮动音量){
Managers.Audio.musicVolume = 音量; ❷
}
......
public void OnMusicToggle() {
Managers.Audio.musicMute = !Managers.Audio.musicMute; ❶
Managers.Audio.PlaySound(sound);
}
public void OnMusicValue(float volume) {
Managers.Audio.musicVolume = volume; ❷
}
...
❶ Repeat the mute control, but use musicMute instead.
❷ Repeat the volume control, but use musicVolume instead.
这段代码不需要太多解释——它主要是重复音量控制。显然,AudioManager属性用法已从soundMute / soundVolume更改为musicMute / musicVolume。
This code doesn’t need a lot of explaining—it’s mostly repeating the sound volume controls. Obviously, the AudioManager properties used have changed from soundMute/ soundVolume to musicMute/musicVolume.
In the editor, create a button and slider, as you did before. Here are those steps again:
将这些 UI 控件链接到SettingsPopup中的代码。在 UI 元素的设置中找到 On Click/OnValueChanged 面板,单击 + 按钮添加条目,将弹出对象拖到对象槽,然后从菜单中选择函数。您需要从菜单的 Dynamic Float 部分中选择OnMusicToggle()和OnMusicValue()函数。
Link these UI controls to the code in SettingsPopup. Find the On Click/OnValueChanged panel in the UI element’s settings, click the + button to add an entry, drag the pop-up object to the object slot, and select the function from the menu. The functions you need to pick are OnMusicToggle() and OnMusicValue() from the Dynamic Float section of the menu.
运行此代码,你会看到控件分别影响音效和音乐。这已经相当复杂了,但还有一点需要完善:音乐之间的淡入淡出階段。
Run this code and you’ll see that the controls affect sound effects and music separately. This is getting pretty sophisticated, but one more bit of polish remains: cross-fade between music tracks.
作为最后再做一点润色,让AudioManager在不同的背景音乐之间淡入淡出。目前,音乐曲目之间的切换非常不协调,声音会突然中断并切换到新曲目。我们可以通过让上一首曲目的音量快速减小,而新曲目的音量从 0 快速上升来平滑过渡。这是一个简单但巧妙的代码,它结合了您刚刚看到的音量控制方法,以及一个协同程序,可以随着时间的推移逐渐改变音量。
As a final bit of polish, let’s make AudioManager fade in and out between different background tunes. Currently, the switch between music tracks is pretty jarring, with the sound suddenly cutting off and changing to the new track. We can smooth out that transition by having the volume of the previous track quickly dwindle away while the volume quickly rises from 0 on the new track. This is a simple but clever bit of code that combines both the volume control methods you just saw, along with a coroutine to change the volume incrementally over time.
清单 11.13 为AudioManager添加了很多功能,但大多数都围绕着一个简单的概念:现在我们有两个独立的音频源,我们将在不同的音频源上播放不同的音乐曲目,并逐步增加一个源的音量,同时降低另一个源的音量。(像往常一样,斜体代码已经在脚本中,并在此处显示以供参考。)
Listing 11.13 adds a lot of bits to AudioManager, but most revolve around a simple concept: now that we have two separate audio sources, we’ll play separate music tracks on separate audio sources, and incrementally increase the volume of one source while simultaneously decreasing the volume of the other. (As usual, italicized code was already in the script and is shown here for reference.)
Listing 11.13 Cross-fading between music in AudioManager
... [SerializeField] AudioSource music2Source; ❶ 私人AudioSource activeMusic; ❷ 私人AudioSource不活动音乐; 公共浮动crossFadeRate = 1.5f; 私有 bool crossFading; ❸ ... 公共浮动音乐音量{ ... 放 { _musicVolume = 值; 如果 (music1Source != null && !crossFading) { music1Source.volume = _musicVolume; music2Source.volume = _musicVolume; ❹ } } } ... 公共 bool musicMute { ... 放 { 如果 (music1Source != null) { 音乐1源.静音 = 值; music2Source.mute = 值; } } } 公共无效启动(NetworkService服务){ Debug.Log("音频管理器正在启动..."); 网络=服务; 音乐1源.ignoreListenerVolume = true; 音乐2源.ignoreListenerVolume = true; 音乐1源.ignoreListenerPause = true; 音乐2源.ignoreListenerPause = true; 声音音量 = 1f; 音乐音量=1f; activeMusic = music1Source; ❺ 非活动音乐 = 音乐2源; 状态 = ManagerStatus.已启动; } ... 私有 void PlayMusic(AudioClip clip) { 如果(crossFading){返回;} StartCoroutine(CrossFadeMusic(clip)); ❻ } 私有 IEnumerator CrossFadeMusic(AudioClip clip) { 交叉淡入淡出 = 真; inactiveMusic.clip = clip; 非活动音乐.音量 = 0; 非活动音乐.播放(); 浮点缩放率 = 交叉淡入淡出率 * 音乐音量; 当(activeMusic.volume > 0){ 活动音乐.音量-= scaledRate * Time.deltaTime; 非活动音乐.音量+=scaledRate*Time.deltaTime; 产量回报 null; ❼ } AudioSource temp = activeMusic; ❽ 活跃音乐=非活跃音乐; 活动音乐.音量 = 音乐音量; 非活动音乐 = 温度; 非活动音乐.停止(); 交叉淡入淡出 = 假; } 公共无效停止音乐(){ 活动音乐.停止(); 非活动音乐.停止(); } ...
... [SerializeField] AudioSource music2Source; ❶ private AudioSource activeMusic; ❷ private AudioSource inactiveMusic; public float crossFadeRate = 1.5f; private bool crossFading; ❸ ... public float musicVolume { ... set { _musicVolume = value; if (music1Source != null && !crossFading) { music1Source.volume = _musicVolume; music2Source.volume = _musicVolume; ❹ } } } ... public bool musicMute { ... set { if (music1Source != null) { music1Source.mute = value; music2Source.mute = value; } } } public void Startup(NetworkService service) { Debug.Log("Audio manager starting..."); network = service; music1Source.ignoreListenerVolume = true; music2Source.ignoreListenerVolume = true; music1Source.ignoreListenerPause = true; music2Source.ignoreListenerPause = true; soundVolume = 1f; musicVolume = 1f; activeMusic = music1Source; ❺ inactiveMusic = music2Source; status = ManagerStatus.Started; } ... private void PlayMusic(AudioClip clip) { if (crossFading) {return;} StartCoroutine(CrossFadeMusic(clip)); ❻ } private IEnumerator CrossFadeMusic(AudioClip clip) { crossFading = true; inactiveMusic.clip = clip; inactiveMusic.volume = 0; inactiveMusic.Play(); float scaledRate = crossFadeRate * musicVolume; while (activeMusic.volume > 0) { activeMusic.volume -= scaledRate * Time.deltaTime; inactiveMusic.volume += scaledRate * Time.deltaTime; yield return null; ❼ } AudioSource temp = activeMusic; ❽ activeMusic = inactiveMusic; activeMusic.volume = musicVolume; inactiveMusic = temp; inactiveMusic.Stop(); crossFading = false; } public void StopMusic() { activeMusic.Stop(); inactiveMusic.Stop(); } ...
❶ Second AudioSource (keep the first, too)
❷ Keep track of which source is active vs. inactive.
❸ A toggle to avoid bugs while a cross-fade is happening
❹ Adjust the volume on both music sources.
❺ Initialize one as the active AudioSource.
❻ Call a coroutine when changing music.
❼ Yield statement pauses for one frame.
❽ Temporary variable to use while swapping active and inactive
第一个添加的是第二个音乐源的变量。同时保留第一个AudioSource对象,复制该对象(确保设置相同——选择 Loop),然后将新对象拖到此 Inspector 插槽中。代码还定义了AudioSource变量 activeMusic和inactiveMusic,但这些是代码中使用的私有变量,不会在 Inspector 中公开。具体来说,这些变量定义在任何给定时间两个音频源中的哪一个被视为活动或非活动。
The first addition is a variable for the second music source. While keeping the first AudioSource object, duplicate that object (make sure the settings are the same—select Loop) and then drag the new object into this Inspector slot. The code also defines the AudioSource variables activeMusic and inactiveMusic, but those are private variables used within the code and not exposed in the Inspector. Specifically, those variables define which of the two audio sources is considered active or inactive at any given time.
代码现在在播放新音乐时调用一个协程。此协程设置新音乐在一个AudioSource上播放,而旧音乐继续在旧AudioSource上播放。然后,协程逐渐增加新音乐的音量,同时逐渐降低旧音乐的音量。交叉淡入淡出完成后(即音量完全交换位置),该函数会交换被视为活动和非活动的音频源。
The code now calls a coroutine when playing new music. This coroutine sets the new music playing on one AudioSource while the old music keeps playing on the old AudioSource. Then, the coroutine incrementally increases the volume of the new music while incrementally decreasing the volume of the old music. Once the cross-fading is complete (that is, the volumes have completely exchanged places), the function swaps which audio source is considered active and inactive.
Great! We’ve completed the background music for our game’s audio system.
Sound effects should be uncompressed audio, and music should be compressed, but use the WAV format for both because Unity applies compression to imported audio.
Audio clips can be 2D sounds that always play the same, or 3D sounds that react to the listener’s position.
The volume of sound effects is easily adjusted globally using Unity’s AudioListener.
You can set the volume on individual audio sources that play music.
You can fade background music in and out by setting the volume on individual audio sources.
本章中的项目将把前几章中的所有内容整合在一起。大多数章节都是相当独立的,我们还没有从头到尾地研究整个游戏。我将引导您整理单独介绍的部分,以便您知道如何从所有这些部分构建一个完整的游戏。
The project in this chapter will tie together everything from previous chapters. Most chapters have been pretty self-contained, and we haven’t taken an end-to-end look at the entire game. I’ll walk you through pulling together pieces that have been introduced separately so that you know how to build a complete game from all of those pieces.
我还将讨论游戏的整体结构,包括切换级别和结束游戏(死亡时显示游戏结束,到达出口时显示成功)。我还将向您展示如何保存游戏,因为随着游戏规模的扩大,保存玩家的进度变得越来越重要。
I’ll also discuss the encompassing structure of the game, including switching levels and ending the game (displaying Game Over when you die, and Success when you reach the exit). And I’ll show you how to save the game, because saving the player’s progress becomes increasingly important as the game grows in size.
警告本章的大部分内容使用了前面章节中详细解释过的任务,因此我将快速介绍这些步骤。如果某些步骤让您感到困惑,请参阅相关章节(例如,关于 UI 的第 7 章)以获取更详细的解释。
WARNING Much of this chapter uses tasks that were explained in detail in previous chapters, so I’ll move through the steps quickly. If certain steps confuse you, refer to the relevant chapter (for example, chapter 7 about the UI) for a more detailed explanation.
本章的项目是一个动作角色扮演游戏的演示(角色扮演游戏)。在这种游戏中,摄像机被放置在高处并向下看(见图 12.1),通过点击鼠标来控制角色前往你想要去的地方。你可能熟悉暗黑破坏神游戏,它是一款类似的动作角色扮演游戏。我又要换一种游戏类型了,这样我们就能在这本书结束前再挤进一种类型!
This chapter’s project is a demo of an action role-playing game (RPG). In this sort of game, the camera is placed high and looks down sharply (see figure 12.1), and the character is controlled by clicking the mouse where you want to go. You may be familiar with the game Diablo, which is an action RPG like this. I’m switching to yet another game genre so that we can squeeze in one more genre before the end of the book!
Figure 12.1 Screenshot of the top-down viewpoint
总的来说,本章中的项目将是迄今为止最大的游戏。它将具有以下特点:
In full, the project in this chapter will be the biggest game yet. It’ll have these features:
Whew, that’s a lot to pack in; good thing this is almost the last chapter!
出色地在第 9 章的项目基础上开发动作 RPG 演示。复制该项目的文件夹并在 Unity 中打开副本以开始工作。或者,如果您直接跳到本章,请下载第 9 章的示例项目以在此基础上进行构建。
We’ll develop the action RPG demo by building on the project from chapter 9. Copy that project’s folder and open the copy in Unity to start working. Or, if you skipped directly to this chapter, download the sample project for chapter 9 to build on that.
我们之所以以第 9 章项目为基础,是因为它最接近我们本章的目标,因此需要的修改最少(与其他项目相比)。最终,我们将整合来自多个章节的资产,因此从技术上讲,这与我们从其中一个项目开始并从第 9 章中提取资产没有太大区别。
The reason we’re building on the chapter 9 project is that it’s the closest to our goal for this chapter and thus will require the least modification (compared to other projects). Ultimately, we’ll pull together assets from several chapters, so technically it’s not that different than if we started with one of those projects and pulled in assets from chapter 9.
Here’s a recap of what’s in the project from chapter 9:
这个长长的功能列表已经涵盖了 RPG 演示中的相当多动作,但我们需要修改或添加更多内容。
This hefty list of features covers quite a bit of the action in the RPG demo already, but we’ll either need to modify or add a bit more.
这前两个修改是更新管理器框架并引入计算机控制的敌人。对于前一个任务,回想一下第 10 章对框架的更新,这意味着这些更新不在第 9 章的项目中。对于后一个任务,回想一下你在第 3 章中编写了一个敌人。
The first two modifications will be to update the managers framework and to bring in computer-controlled enemies. For the former task, recall that updates to the framework were made in chapter 10, which means those updates aren’t in the project from chapter 9. For the latter task, recall that you programmed an enemy in chapter 3.
Updating the managers framework
更新管理者是一个相当简单的任务,所以我们先把它解决掉。IGameManager接口在第10章中进行了修改。
Updating the managers is a fairly simple task, so let’s get that out of the way first. The IGameManager interface was modified in chapter 10.
Listing 12.1 Adjusted IGameManager
公共接口 IGameManager {
ManagerStatus 状态 {获取;}
void Startup(NetworkService服务);
}public interface IGameManager {
ManagerStatus status {get;}
void Startup(NetworkService service);
}
此清单中的代码添加了对NetworkService 的引用,因此还请确保复制该附加脚本;将文件从其在章节 10 项目中的位置拖出(请记住,Unity 项目是磁盘上的文件夹,因此请从那里获取文件),然后将其放入新项目中。现在修改Managers以使用更改后的界面。
The code in this listing adds a reference to NetworkService, so also be sure to copy over that additional script; drag the file from its location in the chapter 10 project (remember, a Unity project is a folder on your disc, so get the file from there), and drop it in the new project. Now modify Managers to work with the changed interface.
Listing 12.2 Changing a bit of code in the Managers script
...
私有 IEnumerator StartupManagers() { ❶
NetworkService 网络 = 新的 NetworkService();
foreach(startSequence 中的 IGameManager 管理器){
管理器.启动(网络);
}
......
private IEnumerator StartupManagers() { ❶
NetworkService network = new NetworkService();
foreach (IGameManager manager in startSequence) {
manager.Startup(network);
}
...
❶ The adjustments are at the beginning of this method.
最后,调整InventoryManager和PlayerManager以反映更改后的接口。下一个清单显示了InventoryManager中修改后的代码;PlayerManager需要相同的代码修改,但名称不同。
Finally, adjust both InventoryManager and PlayerManager to reflect the changed interface. The next listing shows the modified code from InventoryManager; PlayerManager needs the same code modifications but with different names.
清单 12.3 调整InventoryManager以反映IGameManager
Listing 12.3 Adjusting InventoryManager to reflect IGameManager
...
私有网络服务网络;
公共无效启动(NetworkService服务){
Debug.Log("库存管理器正在启动..."); ❶
网络=服务;
项目 = 新词典 <string,int>();
......
private NetworkService network;
public void Startup(NetworkService service) {
Debug.Log("Inventory manager starting..."); ❶
network = service;
items = new Dictionary<string, int>();
...
❶ Same adjustments in both managers, but change names
一旦所有小代码更改都完成,一切都应该像以前一样运行。此更新应该以隐形方式运行,游戏仍将以相同的方式运行。这个调整很容易,但下一个调整将是更难。
Once all the minor code changes are in, everything should still act as before. This update should work invisibly, and the game will still work the same. That adjustment was easy, but the next one will be harder.
除了除了第 10 章中的 NetworkServices调整之外,您还需要第 3 章中的 AI 敌人。实现敌人角色涉及大量脚本和艺术资产,因此您需要导入所有这些资产。
Besides the NetworkServices adjustments from chapter 10, you also need the AI enemy from chapter 3. Implementing enemy characters involved a bunch of scripts and art assets, so you need to import all those assets.
首先,复制这些脚本(记住,WanderingAI和ReactiveTarget是 AI 敌人的行为,Fireball是发射的射弹,敌人攻击PlayerCharacter组件,SceneController处理生成敌人):
First, copy over these scripts (remember, WanderingAI and ReactiveTarget were behaviors for the AI enemy, Fireball was the projectile fired, the enemy attacks the PlayerCharacter component, and SceneController handles spawning enemies):
同样,通过拖入这些文件来获取火焰材质、火球预制件和敌人预制件。如果您从第 11 章而不是第 3 章获取敌人,则可能还需要添加火焰粒子材质。
Similarly, get the Flame material, Fireball prefab, and Enemy prefab by dragging in those files. If you got the enemy from chapter 11 instead of 3, you may also need the added fire particle material.
复制所有必需的资产后,资产之间的链接可能会断开,因此您需要重新链接断开资产中引用的对象以使其正常工作。特别是,检查所有预制件上的脚本,因为它们可能断开了连接。例如,Enemy 预制件在检查器中缺少两个脚本,因此单击圆圈按钮(如图 12.2 所示)从脚本列表中选择WanderingAI和ReactiveTarget。同样,检查 Fireball 预制件并重新链接该脚本(如果需要)。完成脚本后,检查材质和纹理的链接。
After copying over all the required assets, the links between assets will probably be broken, so you’ll need to relink the referenced objects in broken assets to get them to work. In particular, check the scripts on all prefabs because they probably disconnected. For example, the Enemy prefab has two missing scripts in the Inspector, so click the circle button (indicated in figure 12.2) to choose WanderingAI and ReactiveTarget from the list of scripts. Similarly, check the Fireball prefab and relink that script if needed. Once you’re through with the scripts, check the links to materials and textures.
Figure 12.2 Linking a script to a component
现在将SceneController添加到控制器对象,并将 Enemy 预制件拖到 Inspector 中该组件的 Enemy 插槽上。您可能需要将 Fireball 预制件拖到 Enemy 的脚本组件上(选择 Enemy 预制件并在 Inspector 中查看WanderingAI )。同时将PlayerCharacter附加到玩家对象,以便敌人攻击玩家。
Now add SceneController to the controller object and drag the Enemy prefab onto that component’s Enemy slot in the Inspector. You may need to drag the Fireball prefab onto the Enemy’s script component (select the Enemy prefab and look at WanderingAI in the Inspector). Also attach PlayerCharacter to the player object so that enemies will attack the player.
玩游戏时,你会看到敌人四处游荡。敌人向玩家发射火球,尽管它们不会造成太大的伤害;选择 Fireball 预制件并将其 Damage 值设置为10。
Play the game and you’ll see the enemy wandering around. The enemy shoots fireballs at the player, although they won’t do much damage; select the Fireball prefab and set its Damage value to 10.
注意:目前,敌人并不擅长追踪和攻击玩家。在这种情况下,我会首先为敌人提供更宽的视野(使用第 9 章中的点积方法)。最终,您将花费大量时间来完善游戏,其中包括迭代敌人的行为。完善游戏以使其更有趣,虽然这对于游戏的发布至关重要,但这不是您将在本书中做的事情。
NOTE Currently, the enemy isn’t particularly good at tracking down and hitting the player. In this case, I’d start by giving the enemy a wider field of vision (using the dot product approach from chapter 9). Ultimately, you’ll spend a lot of time polishing a game, and that includes iterating on the behavior of enemies. Polishing a game to make it more fun, though crucial for a game to be released, isn’t something you’ll do in this book.
另一个问题是,当您在第 3 章中编写此代码时,玩家的健康是一个临时添加的内容,用于测试。现在游戏有一个PlayerManager,因此请根据下一个清单修改PlayerCharacter,以便在该管理器中处理健康。
The other issue is that when you wrote this code in chapter 3, the player’s health was an ad hoc addition, written for testing. Now the game has a PlayerManager, so modify PlayerCharacter according to the next listing in order to work with health in that manager.
清单 12.4 调整PlayerCharacter以使用PlayerManager中的健康值
Listing 12.4 Adjusting PlayerCharacter to use health in PlayerManager
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 PlayerCharacter:MonoBehaviour {
公共无效伤害(int伤害){
经理.玩家.改变健康(-伤害); ❶
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerCharacter : MonoBehaviour {
public void Hurt(int damage) {
Managers.Player.ChangeHealth(-damage); ❶
}
}
❶使用PlayerManager中的值,而不是PlayerCharacter中的变量。
❶ Use the value in PlayerManager instead of the variable in PlayerCharacter.
此时,您已经拥有一个游戏演示,其中包含来自多个以前项目的组件。场景中添加了一个敌人角色,使游戏更具威胁性。但控件和视点仍然来自第三人称移动演示,因此让我们为行动角色扮演游戏。
At this point, you have a game demo with pieces assembled from multiple previous projects. An enemy character has been added to the scene, making the game more threatening. But the controls and viewpoint are still from the third-person movement demo, so let’s implement point-and-click controls for an action RPG.
这演示需要自上而下的视图和鼠标控制玩家的移动(参见图 12.1)。目前,相机响应鼠标,而玩家响应键盘(如第 8 章中编程的那样),这与本章中想要的正好相反。此外,您将修改变色显示器,以便通过单击设备来操作它们。在这两种情况下,现有代码都与您的需求相差不远;您将对移动和设备脚本进行调整。
This demo needs a top-down view and mouse control of the player’s movement (refer to figure 12.1). Currently, the camera responds to the mouse, whereas the player responds to the keyboard (as programmed in chapter 8), which is the reverse of what you want in this chapter. In addition, you’ll modify the color-changing monitor so that devices are operated by clicking them. In both cases, the existing code isn’t terribly far from what you need; you’ll make adjustments to both the movement and device scripts.
Setting up the top-down view of the scene
第一的,您将把相机抬高到 8 Y 以便将其定位为俯视图。您还将调整OrbitCamera以从相机中删除鼠标控制并仅使用箭头键。
First, you’ll raise the camera to 8 Y to position it for an overhead view. You’ll also adjust OrbitCamera to remove mouse controls from the camera and use only arrow keys.
Listing 12.5 Adjusting OrbitCamera to remove mouse controls
...
无效 LateUpdate() {
rotY -= Input.GetAxis("水平") * rotSpeed; ❶
四元数旋转 = 四元数.欧拉(0, rotY, 0);
变换.位置 = 目标.位置 - (旋转 * 偏移);
变换。观察(目标);
}
......
void LateUpdate() {
rotY -= Input.GetAxis("Horizontal") * rotSpeed; ❶
Quaternion rotation = Quaternion.Euler(0, rotY, 0);
transform.position = target.position - (rotation * offset);
transform.LookAt(target);
}
...
❶ Reverse the direction from before.
随着摄像头进一步升高,玩游戏时的视角将为自上而下。不过,目前,移动控制仍使用键盘,因此让我们编写一个指向和点击的脚本移动。
With the camera raised even higher, the view when you play the game will be top-down. At the moment, though, the movement controls still use the keyboard, so let’s write a script for point-and-click movement.
这此代码的总体思路是自动将玩家移向其目标位置(如图 12.3 所示)。此位置通过单击场景来设置。这样,移动玩家的代码不会直接对鼠标做出反应,而是通过单击间接控制玩家的移动。
The general idea for this code will be to automatically move the player toward its target position (as illustrated in figure 12.3). This position is set by clicking in the scene. In this way, the code that moves the player isn’t directly reacting to the mouse, but the player’s movement is being controlled indirectly by clicking.
Figure 12.3 How point-and-click controls work
注意:此移动算法对 AI 角色也很有用。目标位置可以是角色所走的路径,而不是使用鼠标点击。
NOTE This movement algorithm is useful for AI characters as well. Rather than using mouse clicks, the target position could be on a path that the character follows.
为了实现这一点,创建一个名为PointClickMovement的新脚本并替换RelativeMovement组件在播放器上。通过粘贴整个RelativeMovement来开始编写PointClickMovement代码(因为您仍然需要大部分脚本来处理坠落和动画)。然后,根据此清单调整代码。
To implement this, create a new script called PointClickMovement and replace the RelativeMovement component on the player. Start coding PointClickMovement by pasting in the entirety of RelativeMovement (because you still want most of that script for handling falling and animations). Then, adjust the code according to this listing.
清单 12.6 PointClickMovement脚本中的新移动代码
Listing 12.6 New movement code in PointClickMovement script
...
公共类 PointClickMovement : MonoBehaviour { ❶
...
公募浮动减速=25.0f;
公共浮点目标缓冲区 = 1.5f;
私有浮点数 curSpeed = 0f;
私有 Vector3?targetPos; ❷
...
无效更新(){
Vector3 运动 = Vector3.zero;
if (Input.GetMouseButton(0)) { ❸
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); ❹
射线投射命中鼠标命中;
如果 (Physics.Raycast(ray, out mouseHit)) {
targetPos = mouseHit.point; ❺
当前速度 = 移动速度;
}
}
如果 (targetPos != null) { ❻
如果 (curSpeed > moveSpeed * .5f) { ❼
Vector3 adaptedPos = new Vector3(targetPos.Value.x,
变换.位置.y,目标位置.值.z);
四元数 targetRot = Quaternion.LookRotation(
调整后的位置——变换.位置);
变换.旋转 = 四元数.Slerp(变换.旋转,
targetRot, rotSpeed * Time.deltaTime);
}
运动 = curSpeed * Vector3.forward;
运动 = 变换.TransformDirection(运动);
如果(Vector3.距离(targetPos.值,transform.位置)<
➥目标缓冲区) {
curSpeed -= 减速度 * Time.deltaTime; ❽
如果 (curSpeed <= 0) {
目标位置 = 空;
}
}
}
animator.SetFloat("速度", movement.sqrMagnitude); ❾
......
public class PointClickMovement : MonoBehaviour { ❶
...
public float deceleration = 25.0f;
public float targetBuffer = 1.5f;
private float curSpeed = 0f;
private Vector3? targetPos; ❷
...
void Update() {
Vector3 movement = Vector3.zero;
if (Input.GetMouseButton(0)) { ❸
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); ❹
RaycastHit mouseHit;
if (Physics.Raycast(ray, out mouseHit)) {
targetPos = mouseHit.point; ❺
curSpeed = moveSpeed;
}
}
if (targetPos != null) { ❻
if (curSpeed > moveSpeed * .5f) { ❼
Vector3 adjustedPos = new Vector3(targetPos.Value.x,
transform.position.y, targetPos.Value.z);
Quaternion targetRot = Quaternion.LookRotation(
adjustedPos - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation,
targetRot, rotSpeed * Time.deltaTime);
}
movement = curSpeed * Vector3.forward;
movement = transform.TransformDirection(movement);
if (Vector3.Distance(targetPos.Value, transform.position) <
➥ targetBuffer) {
curSpeed -= deceleration * Time.deltaTime; ❽
if (curSpeed <= 0) {
targetPos = null;
}
}
}
animator.SetFloat("Speed", movement.sqrMagnitude); ❾
...
❶ Correct the name after pasting scripts.
❷ Define this value as “nullable” with the ? symbol.
❸ Set the target position when the mouse clicks.
❹ Raycast at the mouse position.
❺ Set target to the position that was hit.
❻ Move if the target position is set.
❼ Rotate toward the target only while moving quickly.
❽ Decelerate to 0 when close to the target.
❾ Everything stays the same from here down.
Update()方法开头的几乎所有内容被删除了,因为该代码处理键盘移动。请注意,这个新代码有两个主要的if语句:一个在鼠标单击时运行,另一个在设置目标时运行。
Almost everything at the beginning of the Update() method was gutted, because that code was handling keyboard movement. Notice that this new code has two main if statements: one that runs when the mouse clicks, and one that runs when a target is set.
提示 可空值是此脚本中使用的一种方便的编程技巧。请注意,目标位置值定义为Vector3? ,而不仅仅是Vector3;这是用于声明可空值的 C# 语法。某些值类型(例如Vector3)通常不能设置为null ,但您可能会遇到一种情况,在这种情况下,使用null状态表示“未设置值”很有用。在这种情况下,您可以将其设为可空值,允许您将值设置为null ,然后通过键入targetPos.Value来访问底层Vector3(或其他)。
TIP Nullable values are a handy programming trick used in this script. Notice that the target position value is defined as Vector3? instead of just Vector3; this is C# syntax for declaring a nullable value. Some value types (such as Vector3) cannot normally be set to null, but you may encounter a situation where it is useful to have a null state that means “no value is set.” In that case, you can make it a nullable value, allowing you to set the value to null, and then access the underlying Vector3 (or whatever) by typing targetPos.Value.
当鼠标点击时,根据鼠标点击的位置设置目标。这是光线投射的另一个重要用途:确定场景中的哪个点位于鼠标光标下方。目标位置设置为鼠标点击的位置。
When the mouse clicks, set the target according to where the mouse clicked. Here’s yet another great use for raycasting: to determine which point in the scene is under the mouse cursor. The target position is set to where the mouse hits.
至于第二个条件,首先旋转以面向目标。Quaternion.Slerp ()会平滑地旋转以面向目标,而不是立即捕捉到该旋转;同时在减速时锁定旋转(否则,玩家在目标处可能会奇怪地旋转),方法是仅在半速以上时旋转。然后,将前进方向从玩家的本地坐标转换为全局坐标(向前移动)。最后,检查玩家与目标之间的距离:如果玩家几乎已经到达目标,则降低移动速度并最终通过移除目标位置来结束移动。
As for the second conditional, first rotate to face the target. Quaternion.Slerp() rotates smoothly to face the target, rather than immediately snapping to that rotation; also lock rotation while slowing down (otherwise, the player can rotate oddly when at the target) by rotating only when over half-speed. Then, transform the forward direction from the player’s local coordinates to global coordinates (to move forward). Finally, check the distance between the player and the target: if the player has almost reached the target, decrement the movement speed and eventually end the movement by removing the target position.
这负责使用鼠标控制移动玩家。玩游戏来测试一下。接下来,让我们让设备在点击。
This takes care of moving the player by using mouse controls. Play the game to test it out. Next, let’s make devices operate when clicked.
Operating devices by using the mouse
在第 9 章(以及此处,直到我们调整代码)中,设备是通过按键来操作的。相反,它们应该在单击时操作。为此,您首先要创建一个所有设备都将继承的基本脚本;基本脚本将具有鼠标控制,设备将继承该控制。创建一个名为BaseDevice的新脚本并编写以下清单中所示的代码。
In chapter 9 (and here, until we adjust the code), devices were operated by pressing a key. Instead, they should operate when clicked. To do this, you’ll first create a base script that all devices will inherit from; the base script will have the mouse control, and devices will inherit that. Create a new script called BaseDevice and write the code shown in the following listing.
Listing 12.7 BaseDevice script that operates when clicked
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 BaseDevice : MonoBehaviour {
公共浮动半径=3.5f;
void OnMouseUp() { ❶
变换玩家 = GameObject.FindWithTag("玩家").变换;
Vector3 玩家位置 = 玩家.位置;
playerPosition.y = transform.position.y; ❷
如果 (Vector3.Distance(transform.position, playerPosition) < 半径) {
Vector3 方向 = 变换.位置 - 玩家位置;
如果(Vector3.Dot(player.forward,方向)> .5f){
操作(); ❸
}
}
}
公共虚拟void操作(){ ❹
// 特定设备的行为
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BaseDevice : MonoBehaviour {
public float radius = 3.5f;
void OnMouseUp() { ❶
Transform player = GameObject.FindWithTag("Player").transform;
Vector3 playerPosition = player.position;
playerPosition.y = transform.position.y; ❷
if (Vector3.Distance(transform.position, playerPosition) < radius) {
Vector3 direction = transform.position - playerPosition;
if (Vector3.Dot(player.forward, direction) > .5f) {
Operate(); ❸
}
}
}
public virtual void Operate() { ❹
// behavior of the specific device
}
}
❶ Function that runs when clicked
❷ Correction to vertical position
❸ Call Operate() if the player is nearby and facing.
❹ virtual marks a method that inheritance can override.
大部分代码都发生在OnMouseDown中,因为MonoBehaviour在对象被点击时会调用该方法。首先,它会检查与玩家的距离(使用垂直位置校正,就像第 9 章中一样),然后使用点积来查看玩家是否面向设备。Operate ()是一个空壳,由继承此脚本的设备填充。
Most of this code happens inside OnMouseDown because MonoBehaviour calls that method when the object is clicked. First, it checks the distance to the player (with a vertical position correction, just as in chapter 9) and then it uses the dot product to see whether the player is facing the device. Operate() is an empty shell to be filled in by devices that inherit this script.
注意:此代码在场景中查找带有Player标签的对象,因此将此标签分配给玩家对象。标签是检查器顶部的下拉菜单;您也可以定义自定义标签,但默认情况下定义了几个标签,包括Player。选择玩家对象进行编辑,然后选择Player标签。
NOTE This code looks in the scene for an object with the Player tag, so assign this tag to the player object. Tag is a drop-down menu at the top of the Inspector; you can define custom tags as well, but several tags are defined by default, including Player. Select the player object to edit it and then select the Player tag.
现在BaseDevice已编程,您可以修改ColorChangeDevice以从该脚本继承。这是新代码。
Now that BaseDevice is programmed, you can modify ColorChangeDevice to inherit from that script. This is the new code.
清单 12.8 调整ColorChangeDevice以从BaseDevice继承
Listing 12.8 Adjusting ColorChangeDevice to inherit from BaseDevice
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 ColorChangeDevice : BaseDevice { ❶
公共覆盖 void Operate() { ❷
颜色随机 = 新颜色(Random.Range(0f,1f),
随机范围(0f,1f),随机范围(0f,1f));
GetComponent<Renderer>().material.color = 随机;
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ColorChangeDevice : BaseDevice { ❶
public override void Operate() { ❷
Color random = new Color(Random.Range(0f,1f),
Random.Range(0f,1f), Random.Range(0f,1f));
GetComponent<Renderer>().material.color = random;
}
}
❶继承BaseDevice而不是MonoBehaviour。
❶ Inherit BaseDevice instead of MonoBehaviour.
❷ Override this method from the base class.
由于此脚本继承自BaseDevice而不是MonoBehaviour,因此它获得了鼠标控制功能。然后它重写了空的Operate()方法对颜色变化行为进行编程。
Because this script inherits from BaseDevice instead of MonoBehaviour, it gets the mouse control functionality. Then it overrides the empty Operate() method to program the color-changing behavior.
对DoorOpenDevice进行同样的更改(从BaseDevice继承而不是MonoBehaviour,并添加对Operate方法的覆盖)。现在,这些设备将在你单击它们时运行。同时删除玩家的DeviceOperator脚本组件,因为该脚本通过按键来操作设备。
Make the same changes (inherit from BaseDevice instead of MonoBehaviour, and add override to the Operate method) to DoorOpenDevice. Now these devices will operate when you click them. Also remove the player’s DeviceOperator script component, because that script operates devices by pressing the key.
这种新的设备输入带来了移动控制方面的问题:目前,移动目标在鼠标点击时设置,但您不想在点击设备时设置移动目标。您可以使用图层来解决这个问题;与在播放器上设置标签的方式类似,对象可以设置为不同的图层,并且代码可以检查这一点。调整PointClickMovement以检查对象的图层。
This new device input brings up an issue with the movement controls: currently, the movement target is set anytime the mouse clicks, but you don’t want to set the movement target when clicking devices. You can fix this issue by using layers; similar to the way a tag was set on the player, objects can be set to different layers, and the code can check for that. Adjust PointClickMovement to check for the object’s layer.
清单 12.9 调整PointClickMovement中的鼠标点击代码
Listing 12.9 Adjusting mouse-click code in PointClickMovement
...
射线 ray = Camera.main.ScreenPointToRay(Input.mousePosition);
射线投射命中鼠标命中;
如果 (Physics.Raycast(ray, out mouseHit)) {
GameObject hitObject = mouseHit.transform.gameObject; ❶
如果(hitObject.layer == LayerMask.NameToLayer(“Ground”)){ ❶
目标位置 = 鼠标点击.点;
当前速度 = 移动速度;
}
}
......
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit mouseHit;
if (Physics.Raycast(ray, out mouseHit)) {
GameObject hitObject = mouseHit.transform.gameObject; ❶
if (hitObject.layer == LayerMask.NameToLayer("Ground")) { ❶
targetPos = mouseHit.point;
curSpeed = moveSpeed;
}
}
...
❶ Added code; the rest is reference.
此清单在鼠标单击代码中添加了一个条件,以查看单击的对象是否位于 Ground 层上。Layers(与 Tags 类似)是 Inspector 顶部的一个下拉菜单;单击它可查看选项。此外,与 tags 类似,默认情况下已定义多个层。您要创建一个新层,因此请在菜单中选择 Edit Layers。在空的层槽中键入Ground (可能是槽 8;代码中的NameToLayer()将名称转换为层编号,以便您可以使用名称而不是编号)。
This listing adds a conditional inside the mouse-click code to see whether the clicked object is on the Ground layer. Layers (like Tags) is a drop-down menu at the top of the Inspector; click it to see the options. Also, like tags, several layers are already defined by default. You want to create a new layer, so choose Edit Layers in the menu. Type Ground in an empty layer slot (probably slot 8; NameToLayer() in the code converts names into layer numbers so that you can use the name instead of the number).
现在,地面图层已添加到菜单中,请将地面对象设置为地面图层 — 这意味着建筑物的地板,以及玩家可以行走的坡道和平台。选择这些对象,然后在图层菜单中选择地面。
Now that the Ground layer has been added to the menu, set ground objects to the Ground layer—that means the floor of the building, along with the ramps and platforms that the player can walk on. Select those objects, and then select Ground in the Layers menu.
玩游戏时,点击变色显示器时你不会移动。太好了,点击控制完成了!从以前的项目中引入这个项目的另一件事是这用户界面。
Play the game and you won’t move when clicking the color-changing monitor. Great, the point-and-click controls are complete! One more thing to bring into this project from previous projects is the UI.
第九章使用了 Unity 的旧即时模式 GUI,因为这种方法更容易编写代码。但是第 9 章中的 UI 看起来不如第 7 章中的 UI 好看,所以让我们采用该界面系统。新 UI 在视觉上比旧 GUI 更精致;图 12.4 显示了您要创建的界面。
Chapter 9 used Unity’s old immediate-mode GUI because that approach was simpler to code. But the UI from chapter 9 doesn’t look as nice as the one from chapter 7, so let’s bring over that interface system. The newer UI is more visually polished than the old GUI; figure 12.4 shows the interface you’re going to create.
Figure 12.4 The UI for this chapter’s project
首先,您需要设置 UI 图形。一旦 UI 图像全部出现在场景中,您就可以将脚本附加到 UI 对象。我将列出所涉及的步骤,但不会详细介绍;如果您需要复习,请参阅第 7 章。如果需要,请在开始之前安装 TextMeshPro 和 2D Sprite 包(请参阅第 5 章和第 6 章):
First, you’ll set up the UI graphics. Once the UI images are all in the scene, you can attach scripts to the UI objects. I’ll list the steps involved without going into detail; if you need a refresher, refer to chapter 7. If needed, install the TextMeshPro and 2D Sprite packages (refer back to chapters 5 and 6 for these) before starting:
In the Sprite Editor, set a 12-pixel border on all sides (remember to apply changes).
(Optional) Name the object HUD Canvas and switch to 2D view mode.
Create a Text object connected to that canvas (GameObject > UI > Text - TextMeshPro).
Set the Text object’s anchor to top left and the object’s position to 120, -50.
Set the label’s Vertex Color to black, set Font Size to 16, and type Health: as the text.
Create an image connected to that canvas (GameObject > UI > Image).
Position the pop-up image at 0, 0 and scale the pop-up to 250 for width and 150 for height.
注意:回想一下如何在查看 3D 场景和 2D 界面之间切换:切换 2D 视图模式,然后双击画布或建筑物以放大该对象。
NOTE Recall how to switch between viewing the 3D scene and the 2D interface: toggle 2D view mode and double-click either the Canvas or the Building to zoom in on that object.
现在,角落里有 Health 标签,中间有蓝色大弹出窗口。在深入了解 UI 功能之前,让我们先对这些部分进行编程。界面代码将使用第 7 章中的相同 Messenger 系统,因此请复制Messenger脚本.然后创建一个GameEvent脚本。
Now you have the Health label in the corner and the large blue pop-up window in the center. Let’s program these parts first before getting deeper into the UI functionality. The interface code will use the same Messenger system from chapter 7, so copy over the Messenger script. Then create a GameEvent script.
清单 12.10与此Messenger系统一起使用的GameEvent脚本
Listing 12.10 GameEvent script to use with this Messenger system
公共静态类 GameEvent {
公共 const 字符串HEALTH_UPDATED = “HEALTH_UPDATED”;
}public static class GameEvent {
public const string HEALTH_UPDATED = "HEALTH_UPDATED";
}
目前,只定义了一个事件;在本章中,您将添加更多事件。从PlayerManager广播此事件。
For now, only one event is defined; over the course of this chapter, you’ll add a few more events. Broadcast this event from PlayerManager.
Listing 12.11 Broadcasting the health event from PlayerManager
...
公共无效ChangeHealth(int值){
健康+=价值;
如果 (健康 > 最大健康值) {
健康=最大健康值;
} 否则,如果 (健康 < 0) {
健康=0;
}
Messenger.Broadcast(GameEvent.HEALTH_UPDATED); ❶
}
......
public void ChangeHealth(int value) {
health += value;
if (health > maxHealth) {
health = maxHealth;
} else if (health < 0) {
health = 0;
}
Messenger.Broadcast(GameEvent.HEALTH_UPDATED); ❶
}
...
❶ Add a line to the end of this function.
每次ChangeHealth()完成时都会广播该事件,以告知程序的其余部分健康状况已发生变化。您需要调整健康状况标签以响应此事件,因此请创建一个UIController脚本。
The event is broadcast every time ChangeHealth() finishes to tell the rest of the program that the health has changed. You want to adjust the health label in response to this event, so create a UIController script.
Listing 12.12 The script UIController, which handles the interface
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
使用TMPro;
公共类 UIController : MonoBehaviour {
[SerializeField] TMP_Text 健康标签; ❶
[SerializeField] InventoryPopup 弹出;
void OnEnable() { ❷
Messenger.添加监听器(GameEvent.HEALTH_UPDATED,OnHealthUpdated);
}
无效OnDisable(){
Messenger.RemoveListener(GameEvent.HEALTH_UPDATED,OnHealthUpdated);
}
无效开始(){
健康更新时(); ❸
弹出.gameObject.SetActive(false); ❹
}
无效更新(){
if (Input.GetKeyDown(KeyCode.M)) { ❺
bool isShowing = popup.gameObject.activeSelf;
弹出.gameObject.SetActive(!正在显示);
弹出.刷新();
}
}
私有 void OnHealthUpdated() { ❻
字符串消息 = $“健康:{Managers.Player.health}/{Managers.Player.maxHealth}”;
健康标签.文本 = 消息;
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
public class UIController : MonoBehaviour {
[SerializeField] TMP_Text healthLabel; ❶
[SerializeField] InventoryPopup popup;
void OnEnable() { ❷
Messenger.AddListener(GameEvent.HEALTH_UPDATED, OnHealthUpdated);
}
void OnDisable() {
Messenger.RemoveListener(GameEvent.HEALTH_UPDATED, OnHealthUpdated);
}
void Start() {
OnHealthUpdated(); ❸
popup.gameObject.SetActive(false); ❹
}
void Update() {
if (Input.GetKeyDown(KeyCode.M)) { ❺
bool isShowing = popup.gameObject.activeSelf;
popup.gameObject.SetActive(!isShowing);
popup.Refresh();
}
}
private void OnHealthUpdated() { ❻
string message = $"Health: {Managers.Player.health}/{Managers.Player.maxHealth}";
healthLabel.text = message;
}
}
❶ Reference the UI object in the scene.
❷ Set the listener for the health update event.
❸ Call the function manually at startup.
❹ Initialize the pop-up to be hidden.
❺ Toggle the pop-up with the M key.
❻ Event listener calls the function to update the health label.
从Controller对象中删除BasicUI,并将此新脚本附加到Canvas(值得注意的是,不是 Controller对象,它现在应该只有SceneController)。此外,创建一个InventoryPopup脚本(现在添加一个空的公共Refresh()方法;其余的将稍后填写)并将其附加到Inventory Popup对象。现在您可以将弹出窗口拖到Canvas对象的引用槽中 UIController组件(然后对健康标签执行相同操作)。
Remove BasicUI from the Controller object, and attach this new script to the Canvas (notably not the Controller object, which should have only SceneController now). Also, create an InventoryPopup script (add an empty public Refresh() method for now; the rest will be filled in later) and attach it to the Inventory Popup object. Now you can drag the pop-up to the reference slot in the Canvas object’s UIController component (and then do the same for the health label).
当您受伤或使用健康包时,健康标签会发生变化,按 M 键可切换弹出窗口。最后一个需要调整的细节是,单击弹出窗口当前会导致玩家移动;与设备一样,您不想在单击 UI 时设置目标位置。对PointClickMovement进行调整。
The health label changes when you get hurt or use health packs, and pressing the M key toggles the pop-up window. One last detail to adjust is that clicking the pop-up window currently causes the player to move; as with devices, you don’t want to set the target position when the UI has been clicked. Make the adjustment to PointClickMovement.
清单 12.13 检查PointClickMovement中的 UI
Listing 12.13 Checking the UI in PointClickMovement
使用 UnityEngine.EventSystems;
...
无效更新(){
Vector3 运动 = Vector3.zero;
如果 (Input.GetMouseButton(0) && !EventSystem.current.IsPointerOverGameObject()) {
...using UnityEngine.EventSystems;
...
void Update() {
Vector3 movement = Vector3.zero;
if (Input.GetMouseButton(0) && !EventSystem.current.IsPointerOverGameObject()) {
...
请注意,条件检查鼠标是否在 UI 上。这样就完成了界面的整体结构,现在让我们具体处理库存弹出窗口。
Note that the conditional checks whether or not the mouse is on the UI. That completes the overall structure of the interface, so now let’s deal with the inventory pop-up specifically.
Implementing the Inventory pop-up
这弹出窗口目前为空白,但它应该显示玩家的库存(如图 12.5 所示)。以下步骤将创建 UI 对象:
The pop-up window is currently blank, but it should display the player’s inventory (depicted in figure 12.5). These steps will create the UI objects:
Create four images and parent them to the pop-up (that is, drag objects in the Hierarchy).
Position all the images at 0 Y and set X values to -75, -25, 25, and 75.
Position the text labels at 45 Y and set X values to -75, -25, 25, and 75.
Set the text (not the anchor!) to Center alignment, Bottom vertical align, and Height 60.
Enter x2 for all the text labels, set Vertex Color black, and Font Size to 16.
In Resources, set all inventory icons as Sprite (instead of Textures).
Drag these sprites to the Source Image slot of the Image objects (also set Native Size).
Add another text label and two buttons, all parented to the pop-up.
Position this text label at -140, -45 with Right alignment and Middle vertical align.
Type Energy: for the text on this label, set Vertex Color to black, and set Font Size to 14.
Set both buttons to Width 60. For Position, set Y to -50 and X to 0 or 70.
Expand the two buttons in the Hierarchy and type Equip on one button and Use on the other.
Figure 12.5 Diagram of the inventory UI
这些是库存弹出窗口的视觉元素;接下来是代码。将以下内容写入InventoryPopup脚本。
These are the visual elements for the inventory pop-up; next is the code. Write the contents of the following into the InventoryPopup script.
Listing 12.14 Full script for InventoryPopup
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
使用 UnityEngine.UI;
使用 UnityEngine.EventSystems;
使用TMPro;
公共类 InventoryPopup:MonoBehaviour {
[SerializeField] Image[] itemIcons; ❶
[SerializeField] TMP_Text[] itemLabels; ❶
[SerializeField] TMP_Text curItemLabel;
[SerializeField] 按钮装备按钮;
[SerializeField] 按钮使用按钮;
私有字符串 curItem;
公共无效刷新(){
列表<string> itemList = Managers.Inventory.GetItemList();
int len = itemIcons.长度;
对于 (int i = 0; i < len; i++) {
如果 (i < itemList.Count) { ❷
itemIcons[i].gameObject.设置活动(true);
itemLabels[i].gameObject.设置活动(true);
字符串 item = itemList[i];
精灵 sprite = Resources.Load<Sprite>($"Icons/{item}"); ❸
itemIcons[i].sprite = sprite;
itemIcons[i].SetNativeSize(); ❹
int 计数 = 经理.Inventory.GetItemCount(项目);
字符串消息 = $“x{count}”;
如果 (item == Managers.Inventory.equippedItem) {
message = "已装备\n" + message; ❺
}
itemLabels[i].text = 消息;
EventTrigger.Entry entry = new EventTrigger.Entry();
entry.eventID = EventTriggerType.PointerClick; ❻
入口.回调.AddListener((BaseEventData 数据) => {
OnItem(项目); ❼
});
EventTrigger 触发器 = itemIcons[i].GetComponent<EventTrigger>();
触发器.触发器.清除(); ❽
触发器.触发器.添加(条目); ❾
}
别的 {
itemIcons[i].gameObject.SetActive(false); ❿
itemLabels[i].gameObject.SetActive(false); ❿
}
}
如果 (!itemList.Contains(curItem)) {
curItem = 空;
}
如果 (curItem == null) { ⓫
curItemLabel.gameObject.SetActive(false);
装备按钮.gameObject.SetActive(false);
使用按钮.gameObject.设置Active(false);
}
其他 { ⓬
curItemLabel.gameObject.SetActive(true);
装备按钮.gameObject.SetActive(true);
if (curItem == "health") { ⓭
使用按钮.gameObject.SetActive(true);
} 别的 {
使用按钮.gameObject.设置Active(false);
}
curItemLabel.text = $"{curItem}:";
}
}
公共无效OnItem(string item){ ⓮
curItem = 项目;
刷新(); ⓯
}
公共无效OnEquip(){
Managers.Inventory.EquipItem(curItem);
刷新();
}
公共无效OnUse(){
Managers.Inventory.ConsumeItem(curItem);
如果 (curItem == "健康") {
经理.球员.改变健康(25);
}
刷新();
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using TMPro;
public class InventoryPopup : MonoBehaviour {
[SerializeField] Image[] itemIcons; ❶
[SerializeField] TMP_Text[] itemLabels; ❶
[SerializeField] TMP_Text curItemLabel;
[SerializeField] Button equipButton;
[SerializeField] Button useButton;
private string curItem;
public void Refresh() {
List<string> itemList = Managers.Inventory.GetItemList();
int len = itemIcons.Length;
for (int i = 0; i < len; i++) {
if (i < itemList.Count) { ❷
itemIcons[i].gameObject.SetActive(true);
itemLabels[i].gameObject.SetActive(true);
string item = itemList[i];
Sprite sprite = Resources.Load<Sprite>($"Icons/{item}"); ❸
itemIcons[i].sprite = sprite;
itemIcons[i].SetNativeSize(); ❹
int count = Managers.Inventory.GetItemCount(item);
string message = $"x{count}";
if (item == Managers.Inventory.equippedItem) {
message = "Equipped\n" + message; ❺
}
itemLabels[i].text = message;
EventTrigger.Entry entry = new EventTrigger.Entry();
entry.eventID = EventTriggerType.PointerClick; ❻
entry.callback.AddListener((BaseEventData data) => {
OnItem(item); ❼
});
EventTrigger trigger = itemIcons[i].GetComponent<EventTrigger>();
trigger.triggers.Clear(); ❽
trigger.triggers.Add(entry); ❾
}
else {
itemIcons[i].gameObject.SetActive(false); ❿
itemLabels[i].gameObject.SetActive(false); ❿
}
}
if (!itemList.Contains(curItem)) {
curItem = null;
}
if (curItem == null) { ⓫
curItemLabel.gameObject.SetActive(false);
equipButton.gameObject.SetActive(false);
useButton.gameObject.SetActive(false);
}
else { ⓬
curItemLabel.gameObject.SetActive(true);
equipButton.gameObject.SetActive(true);
if (curItem == "health") { ⓭
useButton.gameObject.SetActive(true);
} else {
useButton.gameObject.SetActive(false);
}
curItemLabel.text = $"{curItem}:";
}
}
public void OnItem(string item) { ⓮
curItem = item;
Refresh(); ⓯
}
public void OnEquip() {
Managers.Inventory.EquipItem(curItem);
Refresh();
}
public void OnUse() {
Managers.Inventory.ConsumeItem(curItem);
if (curItem == "health") {
Managers.Player.ChangeHealth(25);
}
Refresh();
}
}
❶ Arrays to reference four images and text labels
❷ Check the inventory list while looping through all UI images.
❸ Load the sprite from Resources.
❹ Resize the image to the native size of the sprite.
❺ Label may say “Equipped” in addition to item count.
❼ Lambda function to trigger differently for each item
❽ Clear the listener to refresh from the clean slate.
❾ Add this listener function to EventTrigger.
❿ Hide this image/text if there’s no item to display.
⓫ Hide buttons if no item is selected.
⓬ Display currently selected item.
⓭ Use button only for health item.
⓮ Function called by mouse click listener
⓯ Refresh the inventory display after making changes.
哇,这真是一个很长的脚本!编写完这个程序后,是时候将界面中的所有内容链接起来了。弹出对象上的脚本组件现在具有各种对象引用,包括两个数组;展开两个数组并将其长度设置为 4(参见图 12.6)。将四个图像拖到图标数组中,并将四个文本标签拖到标签数组中。
Whew, that was a long script! With this programmed, it’s time to link everything in the interface. The script component on the pop-up object now has the various object references, including the two arrays; expand both arrays and set to a length of 4 (see figure 12.6). Drag the four images to the icons array, and drag the four text labels to the labels array.
Figure 12.6 Arrays displayed in the Inspector
注意:如果您不确定哪个对象被拖到哪里(它们看起来都一样),请单击检查器中的插槽以查看层次结构视图中突出显示的对象。
NOTE If you aren’t sure which object was dragged where (they all look the same), click the slot in the Inspector to see that object highlighted in the Hierarchy view.
类似地,组件中的插槽引用弹出窗口底部的文本标签和按钮。链接这些对象后,您将为两个按钮添加OnClick侦听器。将这些事件链接到弹出窗口对象,并根据需要选择OnEquip()或OnUse()。
Similarly, slots in the component reference the text label and buttons at the bottom of the pop-up. After linking those objects, you’ll add OnClick listeners for both buttons. Link these events to the pop-up object, and choose either OnEquip() or OnUse() as appropriate.
最后,添加一个EventTrigger组件到所有四个项目图像。InventoryPopup脚本会修改每个图标上的此组件,因此它们最好有此组件!您将在添加组件 > 事件下找到EventTrigger。(通过单击组件顶角的小齿轮按钮,从一个对象中选择复制组件,然后在另一个对象上粘贴为新组件,复制/粘贴组件可能会更方便。)添加此组件但不要分配事件侦听器,因为这是在InventoryPopup代码中完成的。
Finally, add an EventTrigger component to all four of the item images. The InventoryPopup script modifies this component on each icon, so they better have this component! You’ll find EventTrigger under Add Component > Event. (It may be more convenient to copy/paste the component by clicking the little gear button in the top corner of the component, select Copy Component from one object, and then Paste As New on the other.) Add this component but don’t assign event listeners, because that’s done in the InventoryPopup code.
这样就完成了库存 UI!玩游戏,观察当你收集物品并点击按钮时库存弹出窗口的响应。我们现在已经完成了以前项目的零件组装;接下来我将解释如何构建一个更广泛的游戏从这开始。
That completes the inventory UI! Play the game to watch the inventory pop-up respond when you collect items and click buttons. We’re now finished assembling parts from previous projects; next I’ll explain how to build a more expansive game from this beginning.
现在假设您有一个可以运行的动作角色扮演游戏演示,我们将构建这款游戏的总体结构。我指的是通过多个关卡和通过通关来推进游戏的总体流程。我们从第 9 章的项目中得到的是一个关卡,但本章的路线图指定了三个关卡。
Now that you have a functioning action RPG demo, we’re going to build the overarching structure of this game. By that, I mean the overall flow of the game through multiple levels and progressing through the game by beating levels. What we got from chapter 9’s project was a single level, but the road map for this chapter specified three levels.
这样做将涉及将场景与Managers后端进一步分离,因此您将广播有关管理器的消息(就像PlayerManager广播健康更新一样)。创建一个名为StartupEvent的新脚本(清单 12.15);在单独的脚本中定义这些事件,因为这些事件与可重复使用的Managers系统一起使用,而GameEvent特定于游戏。
Doing this will involve decoupling the scene even further from the Managers backend, so you’ll broadcast messages about the managers (just as PlayerManager broadcasts health updates). Create a new script called StartupEvent (listing 12.15); define these events in a separate script because these events go with the reusable Managers system, whereas GameEvent is specific to the game.
Listing 12.15 The StartupEvent script
公共静态类启动事件{
公共 const 字符串 MANAGERS_STARTED = “MANAGERS_STARTED”;
公共 const 字符串 MANAGERS_PROGRESS = “MANAGERS_PROGRESS”;
}public static class StartupEvent {
public const string MANAGERS_STARTED = "MANAGERS_STARTED";
public const string MANAGERS_PROGRESS = "MANAGERS_PROGRESS";
}
Now it’s time to start adjusting Managers, including broadcasting these new events!
现在,该项目只有一个场景,并且 Game Managers 对象位于该场景中。这样做的问题是每个场景都有自己的一组游戏管理器,而您希望所有场景共享一组游戏管理器。为此,您将创建一个单独的启动场景来初始化管理器,然后与游戏的其他场景共享该对象。
Currently, the project has only one scene, and the Game Managers object is in that scene. The problem with that is that every scene will have its own set of game managers, whereas you want a single set of game managers shared by all scenes. To do that, you’ll create a separate Startup scene that initializes the managers and then shares that object with the other scenes of the game.
我们还需要一个新的管理器来处理游戏进程。创建一个名为MissionManager的新脚本。
We’re also going to need a new manager to handle progress through the game. Create a new script called MissionManager.
Listing 12.16 Creating MissionManager
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
使用 UnityEngine.SceneManagement;
公共类 MissionManager : MonoBehaviour,IGameManager {
公共 ManagerStatus 状态 {获取;私人设置;}
公共 int curLevel {获取;私有设置;}
公共 int maxLevel {获取;私人设置;}
私有网络服务网络;
公共无效启动(NetworkService服务){
Debug.Log("任务管理器正在启动...");
网络=服务;
当前级别 = 0;
最大等级=1;
状态 = ManagerStatus.已启动;
}
公共无效GoToNext(){
如果 (curLevel < maxLevel) { ❶
当前级别++;
字符串名称 = $"Level{curLevel}";
Debug.Log($"正在加载{name}");
SceneManager.LoadScene(名称); ❷
} 别的 {
Debug.Log("最后一级");
}
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class MissionManager : MonoBehaviour, IGameManager {
public ManagerStatus status {get; private set;}
public int curLevel {get; private set;}
public int maxLevel {get; private set;}
private NetworkService network;
public void Startup(NetworkService service) {
Debug.Log("Mission manager starting...");
network = service;
curLevel = 0;
maxLevel = 1;
status = ManagerStatus.Started;
}
public void GoToNext() {
if (curLevel < maxLevel) { ❶
curLevel++;
string name = $"Level{curLevel}";
Debug.Log($"Loading {name}");
SceneManager.LoadScene(name); ❷
} else {
Debug.Log("Last level");
}
}
}
❶ Check if last level reached.
❷ Unity’s command to load a scene
在大多数情况下,此清单中没有什么不寻常的事情发生,但请注意LoadScene()方法接近尾声。虽然我之前提到过这种方法(在第 5 章中),但现在它更重要了。这是 Unity 加载场景文件的方法;在第 5 章中,您使用它来重新加载游戏中的一个场景,但您可以通过传入场景文件的名称来加载任何场景。
For the most part, nothing unusual is going on in this listing, but note the LoadScene() method near the end. Although I mentioned this method before (in chapter 5), it’s more important now. That’s Unity’s method for loading a scene file; in chapter 5, you used it to reload the one scene in the game, but you can load any scene by passing in the name of the scene file.
将此脚本附加到场景中的 Game Managers 对象。同时将新组件添加到Managers脚本中。
Attach this script to the Game Managers object in the scene. Also add the new component to the Managers script.
Listing 12.17 Adding a new component to the Managers script
...
[RequireComponent(typeof(MissionManager))]
公共类管理器:MonoBehaviour {
公共静态 PlayerManager 玩家 {获取;私人设置;}
公共静态 InventoryManager 库存 {获取;私人设置;}
公共静态 MissionManager 任务 {获取;私人设置;}
...
无效唤醒(){
DontDestroyOnLoad(游戏对象); ❶
玩家 = GetComponent<PlayerManager>();
库存 = GetComponent <InventoryManager> ();
任务 = GetComponent<MissionManager>();
开始序列 = 新列表 <IGameManager>();
开始序列.添加(玩家);
开始序列.添加(库存);
开始序列.添加(任务);
启动协同程序(StartupManagers());
}
私有 IEnumerator StartupManagers() {
...
如果 (numReady > lastReady) {
Debug.Log($"进度:{numReady}/{numModules}");
Messenger<int, int>.Broadcast(
StartupEvent.MANAGERS_PROGRESS, numReady, numModules); ❷
}
产量返回 null;
}
Debug.Log("所有管理器已启动");
Messenger.Broadcast(StartupEvent.MANAGERS_STARTED); ❸
}
......
[RequireComponent(typeof(MissionManager))]
public class Managers : MonoBehaviour {
public static PlayerManager Player {get; private set;}
public static InventoryManager Inventory {get; private set;}
public static MissionManager Mission {get; private set;}
...
void Awake() {
DontDestroyOnLoad(gameObject); ❶
Player = GetComponent<PlayerManager>();
Inventory = GetComponent<InventoryManager>();
Mission = GetComponent<MissionManager>();
startSequence = new List<IGameManager>();
startSequence.Add(Player);
startSequence.Add(Inventory);
startSequence.Add(Mission);
StartCoroutine(StartupManagers());
}
private IEnumerator StartupManagers() {
...
if (numReady > lastReady) {
Debug.Log($"Progress: {numReady}/{numModules}");
Messenger<int, int>.Broadcast(
StartupEvent.MANAGERS_PROGRESS, numReady, numModules); ❷
}
yield return null;
}
Debug.Log("All managers started up");
Messenger.Broadcast(StartupEvent.MANAGERS_STARTED); ❸
}
...
❶ Unity’s command to persist an object between scenes
❷ Startup event broadcast with data related to the event.
❸ Startup event broadcast without parameters.
您应该已经熟悉了此代码的大部分内容(添加MissionManager就像添加其他管理器一样),但有两个新部分。一个是发送两个整数值的事件;您之前看到了通用的无值事件和带有单个数字的消息,但您可以使用相同的语法发送任意数量的值。
Most of this code should already be familiar to you (adding MissionManager is like adding other managers), but there are two new parts. One is the event that sends two integer values; you saw both generic valueless events and messages with a single number before, but you can send an arbitrary number of values with the same syntax.
另一段新代码是DontDestroyOnLoad()方法。这是 Unity 提供的在场景之间持久化对象的方法。通常,当新场景加载时,场景中的所有对象都会被清除,但通过在对象上使用DontDestroyOnLoad(),您可以确保该对象在新场景中仍然存在。
The other new bit of code is the DontDestroyOnLoad() method. It’s a method provided by Unity for persisting an object between scenes. Normally, all objects in a scene are purged when a new scene loads, but by using DontDestroyOnLoad() on an object, you ensure that object will still be there in the new scene.
Separate scenes for startup and level
因为游戏管理器对象将在所有场景中持续存在,您必须将管理器与游戏的各个级别分开。在项目视图中,复制场景文件(编辑 > 复制),然后适当地重命名两个文件:一个是Startup,另一个是Level1 。打开 Level1 并删除游戏管理器对象(它将由 Startup 提供)。打开 Startup 并删除除游戏管理器、控制器、主摄像头、HUD 画布和 EventSystem 之外的所有内容。通过移除OrbitCamera组件来调整相机,并将 Clear Flags 菜单从 Skybox 改为 Solid Color。删除 Controller 上的脚本组件,并删除与Canvas关联的UI 对象(健康标签和Inventory Popup ) 。
Because the Game Managers object will persist in all scenes, you must separate the managers from individual levels of the game. In Project view, duplicate the scene file (Edit > Duplicate) and then rename the two files appropriately: one Startup and the other Level1. Open Level1 and delete the Game Managers object (it’ll be provided by Startup). Open Startup and delete everything other than Game Managers, Controller, Main Camera, HUD Canvas, and EventSystem. Adjust the camera by removing the OrbitCamera component, and changing the Clear Flags menu from Skybox to Solid Color. Remove the script components on Controller, and delete the UI objects (health label and Inventory Popup) parented to the Canvas.
UI 当前为空,因此创建一个新的滑块(见图 12.7),然后关闭其 Interactable 设置。Controller 对象不再具有任何脚本组件,因此创建一个新的StartupController脚本(清单 12.18)并将其附加到 Controller 对象。
The UI is currently empty, so create a new slider (see figure 12.7) and then turn off its Interactable setting. The Controller object no longer has any script components, so create a new StartupController script (listing 12.18) and attach that to the Controller object.
Figure 12.7 The Startup scene with everything unnecessary removed
清单 12.18 新的StartupController脚本
Listing 12.18 The new StartupController script
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
使用 UnityEngine.UI;
公共类 StartupController : MonoBehaviour {
[SerializeField] 滑块进度条;
无效OnEnable(){
Messenger<int, int>.AddListener(StartupEvent.MANAGERS_PROGRESS,
管理者进度);
Messenger.AddListener(StartupEvent.MANAGERS_STARTED,
管理员启动时);
}
无效OnDisable(){
Messenger<int, int>.RemoveListener(StartupEvent.MANAGERS_PROGRESS,
管理者进度);
Messenger.RemoveListener(StartupEvent.MANAGERS_STARTED,
管理员启动时);
}
私有 void OnManagersProgress(int numReady,int numModules) {
浮点进度 = (浮点)numReady / numModules;
progressBar.value = progress; ❶
}
私有无效OnManagersStarted(){
经理.任务.GoToNext(); ❷
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class StartupController : MonoBehaviour {
[SerializeField] Slider progressBar;
void OnEnable() {
Messenger<int, int>.AddListener(StartupEvent.MANAGERS_PROGRESS,
OnManagersProgress);
Messenger.AddListener(StartupEvent.MANAGERS_STARTED,
OnManagersStarted);
}
void OnDisable() {
Messenger<int, int>.RemoveListener(StartupEvent.MANAGERS_PROGRESS,
OnManagersProgress);
Messenger.RemoveListener(StartupEvent.MANAGERS_STARTED,
OnManagersStarted);
}
private void OnManagersProgress(int numReady, int numModules) {
float progress = (float)numReady / numModules;
progressBar.value = progress; ❶
}
private void OnManagersStarted() {
Managers.Mission.GoToNext(); ❷
}
}
❶ Update the slider to show loading progress.
❷ Load the next scene after managers have started.
接下来,将 Slider 对象链接到 Inspector 中的插槽。最后要做的准备工作是将两个场景添加到 Build Settings 中。构建应用程序将是下一章的主题,因此现在选择 File > Build Settings 来查看和调整场景列表。单击 Add Open Scenes 按钮将场景添加到列表中(加载两个场景并对每个场景执行此操作)。
Next, link the Slider object to the slot in the Inspector. One last thing to do in preparation is add the two scenes to Build Settings. Building the app will be the topic of the next chapter, so for now choose File > Build Settings to see and adjust the list of scenes. Click the Add Open Scenes button to add a scene to the list (load both scenes and do this for each).
注意:您需要将场景添加到 Build Settings 中,以便可以加载它们。如果不这样做,Unity 将不知道哪些场景可用。您不需要在第 5 章中执行此操作,因为您实际上并没有切换级别 - 您只是重新加载当前场景。
NOTE You need to add the scenes to Build Settings so that they can be loaded. If you don’t, Unity won’t know what scenes are available. You didn’t need to do this in chapter 5 because you weren’t actually switching levels—you were just reloading the current scene.
现在,您可以通过单击“启动”场景中的“播放”来启动游戏。游戏管理器对象将在两个场景中共享。
Now you can launch the game by clicking Play from the Startup scene. The Game Managers object will be shared in both scenes.
警告由于管理器是在启动场景中加载的,因此您始终需要从该场景启动游戏。您可以记住在单击“播放”之前始终打开该场景,但是当您单击“播放”时,此编辑器脚本将自动切换到设置场景:https ://github.com/jhocking/from-unity-wiki/blob/main/SceneAutoLoader.cs 。
WARNING Because the managers are loaded in the Startup scene, you always need to launch the game from that scene. You could remember to always open that scene before clicking Play, but this editor script will automatically switch to a set scene when you click Play: https://github.com/jhocking/from-unity-wiki/blob/main/SceneAutoLoader.cs.
提示默认情况下,光照系统会在加载关卡时重新生成光照贴图。但这仅在您编辑关卡时有效;在游戏运行时加载关卡时不会生成光照贴图。正如您在第 10 章中所做的那样,您可以在光照窗口(窗口 > 渲染 > 光照)中关闭自动光照,然后单击按钮手动烘焙光照贴图(请记住,不要触摸创建的光照数据)。
TIP By default, the lighting system regenerates the lightmaps when the level is loaded. But this works only while you are editing the level; lightmaps won’t be generated when loading levels while the game is running. As you did in chapter 10, you can turn off Auto lighting in the lighting window (Window > Rendering > Lighting) and then click the button to manually bake lightmaps (remember, don’t touch the lighting data that’s created).
这种结构变化处理了不同场景之间游戏管理器的共享,但你仍然没有任何成功或失败的条件这等级。
This structural change handles the sharing of game managers between different scenes, but you still don’t have any success or failure conditions within the level.
到处理关卡完成,您需要在场景中放置一个对象供玩家触摸,并且该对象将在玩家达到目标时通知MissionManager 。这将涉及 UI 响应有关关卡完成的消息,因此请向GameEvent添加另一个条目。
To handle level completion, you’ll put an object in the scene for the player to touch, and that object will inform MissionManager when the player reaches the objective. This will involve the UI responding to a message about level completion, so add another entry to GameEvent.
Listing 12.19 Level Complete added to GameEvent
公共静态类 GameEvent {
公共 const 字符串HEALTH_UPDATED = “HEALTH_UPDATED”;
公共 const 字符串 LEVEL_COMPLETE = “LEVEL_COMPLETE”;
}public static class GameEvent {
public const string HEALTH_UPDATED = "HEALTH_UPDATED";
public const string LEVEL_COMPLETE = "LEVEL_COMPLETE";
}
现在向MissionManager添加一个新方法来跟踪任务目标并广播新的事件消息。
Now add a new method to MissionManager to keep track of mission objectives and broadcast the new event message.
Listing 12.20 Objective method in MissionManager
...
公共无效到达目标(){
// 可以有逻辑来处理多个目标
信使.广播(游戏事件.LEVEL_COMPLETE);
}
......
public void ReachObjective() {
// could have logic to handle multiple objectives
Messenger.Broadcast(GameEvent.LEVEL_COMPLETE);
}
...
Adjust the UIController script to respond to that event.
Listing 12.21 New event listener in UIController
...
[序列化字段] TMP_Text levelEnding;
...
无效OnEnable(){
Messenger.添加监听器(GameEvent.HEALTH_UPDATED,OnHealthUpdated);
Messenger.添加监听器(GameEvent.LEVEL_COMPLETE,OnLevelComplete);
}
无效OnDisable(){
Messenger.RemoveListener(GameEvent.HEALTH_UPDATED,OnHealthUpdated);
Messenger.RemoveListener(GameEvent.LEVEL_COMPLETE,OnLevelComplete);
}
...
无效开始(){
健康更新时();
levelEnding.gameObject.设置Active(false);
弹出.gameObject.设置活动(false);
}
...
私有 void OnLevelComplete() {
启动协同程序(CompleteLevel());
}
私有 IEnumerator CompleteLevel() {
levelEnding.gameObject.SetActive(true);
levelEnding.text = "关卡完成!";
产生返回新的WaitForSeconds(2); ❶
经理.任务.GoToNext();
}
......
[SerializeField] TMP_Text levelEnding;
...
void OnEnable() {
Messenger.AddListener(GameEvent.HEALTH_UPDATED, OnHealthUpdated);
Messenger.AddListener(GameEvent.LEVEL_COMPLETE, OnLevelComplete);
}
void OnDisable() {
Messenger.RemoveListener(GameEvent.HEALTH_UPDATED, OnHealthUpdated);
Messenger.RemoveListener(GameEvent.LEVEL_COMPLETE, OnLevelComplete);
}
...
void Start() {
OnHealthUpdated();
levelEnding.gameObject.SetActive(false);
popup.gameObject.SetActive(false);
}
...
private void OnLevelComplete() {
StartCoroutine(CompleteLevel());
}
private IEnumerator CompleteLevel() {
levelEnding.gameObject.SetActive(true);
levelEnding.text = "Level Complete!";
yield return new WaitForSeconds(2); ❶
Managers.Mission.GoToNext();
}
...
❶ Show the message for two seconds and then go to next level.
您会注意到此清单引用了文本标签。打开Level1场景进行编辑,并创建一个新的 UI 文本对象。此标签将是出现在屏幕中间的关卡完成消息。将此文本设置为 Width 240、 Height 60、 Align 和 Vertical-align 均设置为 Center 、 Vertex Color 为黑色、 Font Size 为22。在文本区域中输入Level Complete!,然后将此文本对象链接到UIController的levelEnding引用。
You’ll notice that this listing has a reference to a text label. Open the Level1 scene to edit it, and create a new UI text object. This label will be a level completion message that appears in the middle of the screen. Set this text to Width 240, Height 60, Center for both Align and Vertical-align, Vertex Color black, and Font Size 22. Type Level Complete! in the text area and then link this text object to the levelEnding reference of UIController.
最后,我们将创建一个对象,玩家触摸该对象即可完成关卡(图 12.8 显示了目标的样子)。这将类似于可收集物品:它需要材料和脚本,然后您将整个东西制作成预制件。
Finally, we’ll create an object that the player touches to complete the level (figure 12.8 shows what the objective looks like). This will be similar to collectible items: it needs a material and a script, and you’ll make the entire thing a prefab.
Figure 12.8 Objective object that the player touches to complete the level
在位置18、1、0处创建一个立方体对象。选择 Box Collider 的 Is Trigger 选项,关闭 Mesh Renderer 中的 Cast 和 Receive Shadows,并将对象设置为 Ignore Raycast 层。创建一个名为 objective 的新材质;将其设为亮绿色,并将着色器设置为 Unlit > Color,以获得平坦明亮的外观。接下来,创建ObjectiveTrigger脚本并将该脚本附加到立方体对象。
Create a cube object at Position 18, 1, 0. Select the Is Trigger option of the Box Collider, turn off both Cast and Receive Shadows in Mesh Renderer, and set the object to the Ignore Raycast layer. Create a new material called objective; make it bright green and set the shader to Unlit > Color for a flat, bright look. Next, create the ObjectiveTrigger script and attach that script to the cube object.
清单 12.22 ObjectiveTrigger放置在目标对象上的代码
Listing 12.22 Code for ObjectiveTrigger to put on objective objects
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 ObjectiveTrigger:MonoBehaviour {
void OnTriggerEnter(Collider 其他){
经理.任务.达到目标(); ❶
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ObjectiveTrigger : MonoBehaviour {
void OnTriggerEnter(Collider other) {
Managers.Mission.ReachObjective(); ❶
}
}
❶ Call the new objective method in MissionManager.
将此对象从层次结构拖到项目视图中,将其转换为预制件;在以后的关卡中,您可以将预制件放入场景中。现在玩游戏并达到目标。完成消息会在您通关时显示。接下来,让我们设置一条失败消息,以在您通关时显示失去。
Drag this object from the Hierarchy into Project view to turn it into a prefab; in future levels, you could put the prefab in the scene. Now play the game and go reach the objective. The completion message shows when you beat the level. Next, let’s have a failure message to show when you lose.
这失败条件是玩家耗尽生命值(因为敌人攻击)。首先,在GameEvent中添加另一个条目:
The failure condition will be when the player runs out of health (because of the enemy attacking). First, add another entry in GameEvent:
公共 const 字符串 LEVEL_FAILED = “LEVEL_FAILED”;
public const string LEVEL_FAILED = "LEVEL_FAILED";
现在调整PlayerManager,当玩家的生命值降至0时广播此消息。
Now adjust PlayerManager to broadcast this message when the player’s health drops to 0.
Listing 12.23 Broadcast level failed from PlayerManager
...
公共无效启动(NetworkService服务){
Debug.Log("玩家管理器正在启动...");
网络=服务;
更新数据(50,100); ❶
状态 = ManagerStatus.已启动;
}
公共无效更新数据(int health,int maxHealth){
这个.健康=健康;
这个.最大健康值 = 最大健康值;
}
公共无效ChangeHealth(int值){
健康+=价值;
如果 (健康 > 最大健康值) {
健康=最大健康值;
} 否则,如果 (健康 < 0) {
健康=0;
}
如果 (健康 == 0) {
信使.广播(游戏事件.LEVEL_FAILED);
}
信使.广播(游戏事件.HEALTH_UPDATED);
}
公共无效重生(){ ❷
更新数据(50,100);
}
......
public void Startup(NetworkService service) {
Debug.Log("Player manager starting...");
network = service;
UpdateData(50, 100); ❶
status = ManagerStatus.Started;
}
public void UpdateData(int health, int maxHealth) {
this.health = health;
this.maxHealth = maxHealth;
}
public void ChangeHealth(int value) {
health += value;
if (health > maxHealth) {
health = maxHealth;
} else if (health < 0) {
health = 0;
}
if (health == 0) {
Messenger.Broadcast(GameEvent.LEVEL_FAILED);
}
Messenger.Broadcast(GameEvent.HEALTH_UPDATED);
}
public void Respawn() { ❷
UpdateData(50, 100);
}
...
❶ Call the update method instead of setting variables directly.
❷ Reset the player to the initial state.
向MissionManager添加一个方法,用于重新启动级别。
Add a method to MissionManager for restarting the level.
清单 12.24 MissionManager,可以重新启动当前关卡
Listing 12.24 MissionManager, which can restart the current level
...
公共无效RestartCurrent(){
字符串名称 = $"Level{curLevel}";
Debug.Log($"正在加载{name}");
场景管理器.加载场景(名称);
}
......
public void RestartCurrent() {
string name = $"Level{curLevel}";
Debug.Log($"Loading {name}");
SceneManager.LoadScene(name);
}
...
With that in place, add another event listener to UIController.
Listing 12.25 Responding to failed level in UIController
...
Messenger.添加监听器(GameEvent.LEVEL_FAILED,OnLevelFailed);
...
Messenger.RemoveListener(GameEvent.LEVEL_FAILED,OnLevelFailed);
...
私有 void OnLevelFailed() {
启动协同程序(FailLevel());
}
私有 IEnumerator FailLevel() {
levelEnding.gameObject.SetActive(true);
levelEnding.text = "关卡失败"; ❶
产生返回新的WaitForSeconds(2);
经理.玩家.重生();
经理.任务.RestartCurrent(); ❷
}
......
Messenger.AddListener(GameEvent.LEVEL_FAILED, OnLevelFailed);
...
Messenger.RemoveListener(GameEvent.LEVEL_FAILED, OnLevelFailed);
...
private void OnLevelFailed() {
StartCoroutine(FailLevel());
}
private IEnumerator FailLevel() {
levelEnding.gameObject.SetActive(true);
levelEnding.text = "Level Failed"; ❶
yield return new WaitForSeconds(2);
Managers.Player.Respawn();
Managers.Mission.RestartCurrent(); ❷
}
...
❶ Reuse the same text label, but set a different message.
❷ Restart the current level after a two-second pause.
玩游戏,让敌人向你射击几次;关卡失败信息将会出现。干得好——玩家现在可以完成关卡并失败!在此基础上,游戏必须跟踪玩家的进步。
Play the game and let the enemy shoot you several times; the level failure message will appear. Great job—the player can now complete and fail levels! Building off that, the game must keep track of the player’s progress.
正确的现在,各个关卡独立运行,与整个游戏没有任何关系。您将添加两项功能,让游戏进程感觉更加完整:保存玩家的进度并检测游戏(而不仅仅是关卡)何时完成。
Right now, the individual level operates independently, without any relation to the overall game. You’ll add two things that will make progress through the game feel more complete: saving the player’s progress and detecting when the game (not just the level) is complete.
保存加载游戏是大多数游戏的一个重要部分。Unity 和 Mono 提供了可用于此目的的 I/O 功能。不过,在开始使用此功能之前,您必须为MissionManager和InventoryManager添加UpdateData() 。该方法将像在PlayerManager中一样工作,并将使管理器外部的代码能够更新管理器内的数据。清单 12.26 和清单 12.27 显示了更改后的管理器。
Saving and loading the game is an important part of most games. Unity and Mono provide I/O functionality that you can use for this purpose. Before you can start using that, though, you must add UpdateData() for both MissionManager and InventoryManager. That method will work as it does in PlayerManager and will enable code outside the manager to update data within the manager. Listing 12.26 and listing 12.27 show the changed managers.
清单 12.26 MissionManager中的UpdateData()方法
Listing 12.26 UpdateData() method in MissionManager
...
公共无效启动(NetworkService服务){
Debug.Log("任务管理器正在启动...");
网络=服务;
更新数据(0,1); ❶
状态 = ManagerStatus.已启动;
}
公共无效更新数据(int curLevel,int maxLevel){
这个.curLevel = curLevel;
这个.最大等级 = 最大等级;
}
......
public void Startup(NetworkService service) {
Debug.Log("Mission manager starting...");
network = service;
UpdateData(0, 1); ❶
status = ManagerStatus.Started;
}
public void UpdateData(int curLevel, int maxLevel) {
this.curLevel = curLevel;
this.maxLevel = maxLevel;
}
...
❶ Modify this line by using the new method.
清单 12.27 InventoryManager中的UpdateData()方法
Listing 12.27 UpdateData() method in InventoryManager
...
公共无效启动(NetworkService服务){
Debug.Log("库存管理器正在启动...");
网络=服务;
更新数据(new Dictionary <string, int>()); ❶
状态 = ManagerStatus.已启动;
}
公共无效更新数据(字典<string,int>项){
这.物品=物品;
}
公共字典<string, int> GetData() { ❷
退货;
}
......
public void Startup(NetworkService service) {
Debug.Log("Inventory manager starting...");
network = service;
UpdateData(new Dictionary<string, int>()); ❶
status = ManagerStatus.Started;
}
public void UpdateData(Dictionary<string, int> items) {
this.items = items;
}
public Dictionary<string, int> GetData() { ❷
return items;
}
...
❷ Need getter for save game code to access the data.
现在各种管理器都有UpdateData()方法,可以从新代码模块保存数据。保存数据将涉及称为序列化数据的过程。
Now that the various managers all have UpdateData() methods, the data can be saved from a new code module. Saving the data will involve a procedure referred to as serializing the data.
DEFINITION Serialize means to encode a batch of data into a form that can be stored.
您将把游戏保存为二进制数据,但请注意,C# 也完全能够保存文本文件。例如,您在第 10 章中使用的 JSON 字符串是序列化为文本的数据。前面的章节使用了PlayerPrefs,但在这个项目中,您将保存一个本地文件;PlayerPrefs旨在仅保存少数值(例如设置),而不是整个游戏。创建DataManager脚本(清单 12.28)。
You’ll save the game as binary data, but note that C# is also fully capable of saving text files. For example, the JSON strings you worked with in chapter 10 were data serialized as text. Previous chapters used PlayerPrefs, but in this project, you’re going to save a local file; PlayerPrefs is intended to save only a handful of values, like settings, not an entire game. Create the DataManager script (listing 12.28).
警告您无法直接访问网页游戏中的文件系统。这是网页浏览器的安全功能。要保存网页游戏的数据,您可能需要编写一个插件(如下一章所述),或将数据发布到您的服务器。
WARNING You can’t directly access the filesystem in a web game. This is a security feature of web browsers. To save data for web games, you may need to write a plugin as described in the next chapter, or post the data to your server.
Listing 12.28 New script for DataManager
使用System.Collections;
使用 System.Collections.Generic;
使用 System.Runtime.Serialization.Formatters.Binary;
使用System.IO;
使用 UnityEngine;
公共类 DataManager : MonoBehaviour,IGameManager {
公共 ManagerStatus 状态 {获取;私人设置;}
私有字符串文件名;
私有网络服务网络;
公共无效启动(NetworkService服务){
Debug.Log("数据管理器正在启动...");
网络=服务;
文件名 = Path.Combine(
Application.persistentDataPath,“game.dat”); ❶
状态 = ManagerStatus.已启动;
}
公共无效SaveGameState(){
字典 <string, object> 游戏状态 =
新 Dictionary <string,object> (); ❷
游戏状态.添加(“库存”,经理.库存.获取数据());
游戏状态.添加(“健康”,经理.玩家.健康);
游戏状态.添加(“maxHealth”,Managers.Player.maxHealth);
游戏状态.添加(“curLevel”, Managers.Mission.curLevel);
游戏状态.添加(“maxLevel”,经理.任务.maxLevel);
使用(FileStream stream = File.Create(filename)){ ❸
BinaryFormatter 格式化程序 = 新 BinaryFormatter();
格式化程序.Serialize(流,游戏状态); ❹
}
}
公共无效LoadGameState(){
if (!File.Exists(filename)) { ❺
Debug.Log("没有保存的游戏");
返回;
}
字典<string,object> gamestate; ❻
使用(FileStream stream = File.Open(文件名,FileMode.Open)){
BinaryFormatter 格式化程序 = 新 BinaryFormatter();
gamestate = formatter.Deserialize(stream) 作为 Dictionary<string,
➥对象>;
}
经理.库存.更新数据((字典<string,
➥ int>)游戏状态["库存"]); ❼
经理.玩家.更新数据((int)游戏状态["健康"],
➥ (int) gamestate["maxHealth"]);
经理.任务.更新数据((int)游戏状态["curLevel"],
➥ (int) gamestate["maxLevel"]);
管理者.任务.重新启动当前();
}
}using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using UnityEngine;
public class DataManager : MonoBehaviour, IGameManager {
public ManagerStatus status {get; private set;}
private string filename;
private NetworkService network;
public void Startup(NetworkService service) {
Debug.Log("Data manager starting...");
network = service;
filename = Path.Combine(
Application.persistentDataPath, "game.dat"); ❶
status = ManagerStatus.Started;
}
public void SaveGameState() {
Dictionary<string, object> gamestate =
new Dictionary<string, object>(); ❷
gamestate.Add("inventory", Managers.Inventory.GetData());
gamestate.Add("health", Managers.Player.health);
gamestate.Add("maxHealth", Managers.Player.maxHealth);
gamestate.Add("curLevel", Managers.Mission.curLevel);
gamestate.Add("maxLevel", Managers.Mission.maxLevel);
using (FileStream stream = File.Create(filename)) { ❸
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, gamestate); ❹
}
}
public void LoadGameState() {
if (!File.Exists(filename)) { ❺
Debug.Log("No saved game");
return;
}
Dictionary<string, object> gamestate; ❻
using (FileStream stream = File.Open(filename, FileMode.Open)) {
BinaryFormatter formatter = new BinaryFormatter();
gamestate = formatter.Deserialize(stream) as Dictionary<string,
➥ object>;
}
Managers.Inventory.UpdateData((Dictionary<string,
➥ int>)gamestate["inventory"]); ❼
Managers.Player.UpdateData((int)gamestate["health"],
➥ (int)gamestate["maxHealth"]);
Managers.Mission.UpdateData((int)gamestate["curLevel"],
➥ (int)gamestate["maxLevel"]);
Managers.Mission.RestartCurrent();
}
}
❶ Construct full path to the game.dat file.
❷ Dictionary that will be serialized
❸ Create a file at the file path.
❹ Serialize the Dictionary as contents of the created file.
❺ Continue to load only if the file exists.
❻ Dictionary to put loaded data in
❼ Update managers with deserialized data.
在Startup()期间,使用Application.persistentDataPath构建完整文件路径,这是 Unity 提供的用于存储数据的位置。确切的文件路径在不同平台上有所不同,但 Unity 将其抽象到这个静态变量后面。File.Create ()方法将创建一个二进制文件;如果需要文本文件,请调用File.CreateText() 。
During Startup(), the full file path is constructed using Application.persistentDataPath, a location Unity provides to store data in. The exact file path differs on different platforms, but Unity abstracts it behind this static variable. The File.Create() method will create a binary file; call File.CreateText() if you want a text file.
警告构造文件路径时,路径分隔符在不同的计算机平台上是不同的。C# 有Path.DirectorySeparatorChar来解决这个问题。
WARNING When constructing file paths, the path separator is different on different computer platforms. C# has Path.DirectorySeparatorChar to account for this.
打开启动场景,找到 Game Managers。将DataManager脚本组件添加到 Game Managers 对象,然后将新管理器添加到Managers脚本。
Open the Startup scene to find Game Managers. Add the DataManager script component to the Game Managers object, and then add the new manager to the Managers script.
Listing 12.29 Adding DataManager to Managers
...
[RequireComponent(typeof(DataManager))]
...
公共静态 DataManager 数据 {获取;私有设置;}
...
无效唤醒(){
不要在加载时销毁(游戏对象);
数据 = GetComponent<DataManager>();
玩家 = GetComponent<PlayerManager>();
库存 = GetComponent <InventoryManager> ();
任务 = GetComponent<MissionManager>();
startSequence = new List<IGameManager>(); ❶
开始序列.添加(玩家);
开始序列.添加(库存);
开始序列.添加(任务);
开始序列.添加(数据);
启动协同程序(StartupManagers());
}
......
[RequireComponent(typeof(DataManager))]
...
public static DataManager Data {get; private set;}
...
void Awake() {
DontDestroyOnLoad(gameObject);
Data = GetComponent<DataManager>();
Player = GetComponent<PlayerManager>();
Inventory = GetComponent<InventoryManager>();
Mission = GetComponent<MissionManager>();
startSequence = new List<IGameManager>(); ❶
startSequence.Add(Player);
startSequence.Add(Inventory);
startSequence.Add(Mission);
startSequence.Add(Data);
StartCoroutine(StartupManagers());
}
...
❶ Managers start in this order.
警告由于DataManager使用其他管理器(来更新它们),所以您应该确保其他管理器在启动序列中更早出现。
WARNING Because DataManager uses other managers (to update them), you should make sure that the other managers appear earlier in the startup sequence.
最后,在Level1中添加按钮以使用DataManager中的函数(图 12.9 显示了这些按钮)。创建两个按钮并将其父级设为 HUD Canvas(不在 Inventory 弹出窗口中)。将它们命名为(设置附加的文本对象)Save Game和Load Game,将 Anchor 按钮设置为右下角,并将它们定位在-100,65和-100,30处。
Finally, in Level1, add buttons to use functions in DataManager (figure 12.9 shows the buttons). Create two buttons parented to the HUD Canvas (not in the Inventory pop-up). Call them (set the attached text objects) Save Game and Load Game, set the Anchor button to bottom right, and position them at -100,65, and -100,30.
Figure 12.9 Save and Load buttons on the bottom right of the screen
这些按钮将链接到UIController中的函数,因此请编写这些方法。
These buttons will link to functions in UIController, so write those methods.
清单 12.30 UIController中的保存和加载方法
Listing 12.30 Save and load methods in UIController
...
公共无效保存游戏(){
管理员.数据.保存游戏状态();
}
公共无效加载游戏(){
管理员.数据.加载游戏状态();
}
......
public void SaveGame() {
Managers.Data.SaveGameState();
}
public void LoadGame() {
Managers.Data.LoadGameState();
}
...
将这些函数链接到按钮中的OnClick侦听器(在OnClick设置中添加列表,拖入UIController对象,然后从菜单中选择函数)。现在玩游戏,拿起一些物品,使用健康包来增加健康,然后保存游戏。重新启动游戏并检查您的库存以验证它是否为空。单击加载;您现在拥有了保存游戏时的健康值和物品游戏!
Link these functions to OnClick listeners in the buttons (add a listing in the OnClick setting, drag in the UIController object, and select functions from the menu). Now play the game, pick up a few items, use a health pack to increase your health, and then save the game. Restart the game and check your inventory to verify that it’s empty. Click Load; you now have the health and items you had when you saved the game!
作为正如我们保存玩家进度所暗示的那样,这个游戏可以有多个关卡,而不仅仅是你一直在测试的一个关卡。为了正确处理多个关卡,游戏必须检测不仅单个关卡的完成情况,还要检测整个游戏的完成情况。首先,添加另一个GameEvent:
As implied by our saving of the player’s progress, this game can have multiple levels, not just the one level you’ve been testing. To properly handle multiple levels, the game must detect the completion of not only a single level, but also the entire game. First, add yet another GameEvent:
公共 const 字符串 GAME_COMPLETE = "GAME_COMPLETE";
public const string GAME_COMPLETE = "GAME_COMPLETE";
现在修改MissionManager以在最后一级之后广播该消息。
Now modify MissionManager to broadcast that message after the last level.
清单 12.31 从MissionManager广播游戏完成
Listing 12.31 Broadcasting Game Complete from MissionManager
...
公共无效GoToNext(){
...
} 别的 {
Debug.Log("最后一级");
信使.广播(游戏事件.GAME_COMPLETE);
}
}...
public void GoToNext() {
...
} else {
Debug.Log("Last level");
Messenger.Broadcast(GameEvent.GAME_COMPLETE);
}
}
Respond to that message in UIController.
Listing 12.32 Adding an event listener to UIController
...
Messenger.添加监听器(GameEvent.GAME_COMPLETE,OnGameComplete);
...
Messenger.RemoveListener(GameEvent.GAME_COMPLETE,OnGameComplete);
...
私有 void OnGameComplete() {
levelEnding.gameObject.SetActive(true);
levelEnding.text = "你已完成游戏!";
}
......
Messenger.AddListener(GameEvent.GAME_COMPLETE, OnGameComplete);
...
Messenger.RemoveListener(GameEvent.GAME_COMPLETE, OnGameComplete);
...
private void OnGameComplete() {
levelEnding.gameObject.SetActive(true);
levelEnding.text = "You Finished the Game!";
}
...
尝试完成关卡,看看会发生什么:像以前一样将玩家移至关卡目标以完成关卡。您首先会看到“关卡完成”消息,但几秒钟后,它会变成有关完成游戏的消息。
Try completing the level to see what happens: move the player to the level objective to complete the level as before. You’ll first see the Level Complete message, but after a couple of seconds, it’ll change to a message about completing the game.
在此时,您可以添加任意数量的其他关卡,MissionManager将监视最后一个关卡。您在本章中要做的最后一件事是向项目添加更多关卡,以演示游戏如何通过多个关卡。
At this point, you can add an arbitrary number of additional levels, and MissionManager will watch for the last level. The final thing you’ll do in this chapter is add a few more levels to the project to demonstrate the game progressing through multiple levels.
复制Level1场景文件两次,确保名称为Level2和Level3,并将新关卡添加到 Build Settings(以便它们可以在游戏过程中加载;记得生成照明)。修改每个场景,以便您可以区分各个关卡;您可以随意重新排列大部分场景,但必须保留几个基本游戏元素:标记为 Player 的玩家对象、设置为 Ground 层的地板对象、目标对象、Controller、HUD Canvas 和 EventSystem。
Duplicate the Level1 scene file twice, make sure the names are Level2 and Level3, and add the new levels to Build Settings (so that they can be loaded during gameplay; remember to generate the lighting). Modify each scene so that you can tell the difference between levels; feel free to rearrange most of the scene, but you must keep several essential game elements: the player object that’s tagged Player, the floor object set to the Ground layer, and the objective object, Controller, HUD Canvas, and EventSystem.
您还需要调整MissionManager以加载新关卡。通过将UpdateData(0, 1)调用更改为UpdateData(0, 3)来将maxLevel更改为 3。现在玩游戏,您将首先从Level1开始;达到关卡目标,您将进入下一关!顺便说一句,您还可以在以后的关卡中保存,以查看游戏是否会恢复该进度。
You also need to adjust MissionManager to load the new levels. Change maxLevel to 3 by changing the UpdateData(0, 1) call to UpdateData(0, 3). Now play the game and you’ll start on Level1 initially; reach the level objective and you’ll move on to the next level! Incidentally, you can also save on a later level to see that the game will restore that progress.
现在,您已经知道如何创建具有多个级别的完整游戏。显然,下一个任务是最后一章:让您的游戏进入手的玩家。
You now know how to create a full game with multiple levels. The obvious next task is the final chapter: getting your game into the hands of players.
Unity makes it easy to repurpose assets and code from a project in a different game genre.
Another great use for raycasting is to determine where in the scene the player is clicking.
Unity has simple methods for both loading levels and persisting certain objects between levels.
You progress through levels in response to various events within the game.
You can use the I/O methods that come with C# to store data at Application .persistentDataPath.
通过本书,您已经学会了如何在 Unity 中编写各种游戏,但到目前为止,关键的最后一步还未完成:将这些游戏部署给玩家。除非游戏可以在 Unity 编辑器之外玩,否则除了开发人员之外,其他人对它的兴趣不大。Unity 在这最后一步上表现出色,能够为各种游戏平台构建应用程序。最后一章将介绍如何为这些不同的平台构建游戏。
Throughout this book, you’ve learned how to program various games within Unity, but the crucial last step has been missing so far: deploying those games to players. Until a game is playable outside the Unity editor, it’s of little interest to anyone other than the developer. Unity shines at this last step, with the ability to build applications for a huge variety of gaming platforms. This final chapter covers how to build games for these various platforms.
当我提到为某个平台“构建”时,我指的是生成可在该平台上运行的应用程序包。在每个平台(Windows、iOS 等)上,构建的应用程序的确切形式各不相同,但生成可执行文件后,该应用程序包无需 Unity 即可运行,并可分发给玩家。单个 Unity 项目可以部署到任何平台,而无需为每个平台重新构建。
When I speak of “building” for a platform, I’m referring to generating an application package that will run on that platform. On each platform (Windows, iOS, and so on), the exact form of a built application differs, but once the executable has been generated, that app package can be played without Unity and can be distributed to players. A single Unity project can be deployed to any platform without needing to be redone for each one.
这种“一次构建,随处部署”的功能适用于游戏中的绝大多数功能,但并非适用于所有功能。我估计,在 Unity 中编写的代码中有 95%(例如,本书中迄今为止我们所做的几乎所有代码)与平台无关,并且在所有平台上都能很好地运行。但不同平台的一些特定任务有所不同,因此我们将讨论这些特定于平台的开发领域。
This “build once, deploy anywhere” capability applies to the vast majority of features in your games, but not to everything. I would estimate that 95% of the code written in Unity (for example, almost everything we’ve done so far in this book) is platform-agnostic and will work just as well across all platforms. But a few specific tasks differ for different platforms, so we’ll go over those platform-specific areas of development.
Unity is capable of building apps for the following platforms:
此外,通过联系平台所有者获取访问权限,Unity 甚至可以为如下游戏机构建游戏:
In addition, by contacting the platform owners for access, Unity can even build for game consoles like these:
哇,这个完整列表真的很长!坦率地说,这几乎长得可笑,而且比市面上大多数其他游戏开发工具支持的平台要多得多。本章特别关注列出的前六个平台,因为这些平台是大多数探索 Unity 的人的主要兴趣所在,但请记住,您有多少选择。
Whew, that full list is really long! Frankly, that’s almost comically long, and way more than the supported platforms of most other game development tools out there. This chapter focuses especially on the first six platforms listed, because those platforms are of primary interest to the majority of people exploring Unity, but keep in mind how many options are available to you.
要查看所有这些平台,请打开“构建设置”窗口。这是您在上一章中用来添加要加载的场景的窗口;要访问它,请选择“文件”>“构建设置”。在第 12 章中,您只关心顶部的列表,但现在您要关注底部的按钮(见图 13.1)。您会注意到平台列表占用了大量空间;当前活动的平台用 Unity 图标表示。
To see all these platforms, open the Build Settings window. That’s the window you used in the previous chapter to add scenes to be loaded; to access it, choose File > Build Settings. In chapter 12, you cared only about the list at the top, but now you want to pay attention to the buttons at the bottom (see figure 13.1). You’ll notice a lot of space taken up by the list of platforms; the currently active platform is indicated with the Unity icon.
Figure 13.1 The Build Settings window
注意:安装 Unity 时,Unity Hub 会询问您需要哪些导出模块,并且您只能构建选定的模块。如果您稍后想要安装最初未选择的模块,请转到 Unity Hub 中的安装,单击要修改的 Unity 版本的三个点,然后在菜单中选择添加模块。
NOTE When installing Unity, Unity Hub asks which export modules you want, and you can build only the selected modules. If you later want to install a module you hadn’t selected initially, go to Installs in Unity Hub, click the three dots for the Unity version you want to modify, and then select Add Modules in the menu.
此窗口底部还有“Player Settings”(播放器设置)和“Build/Switch Platform”(构建/切换平台)按钮。单击“Player Settings”(播放器设置)可在 Inspector 中查看应用的设置,例如应用的名称和图标。另一个按钮的标签会根据您在平台列表中选择的平台而改变。如果您选择了活动平台,单击“Build”(构建)即可启动构建过程。对于任何其他平台,单击“Switch Platform”(切换平台)可使其成为 Unity 当前正在处理的活动平台。
Also across the bottom of this window are the Player Settings and Build/Switch Platform buttons. Click Player Settings to view settings for the app in the Inspector, such as the name and icon for the app. The other button changes its label depending on which platform you select in the list of platforms. If you have the active platform selected, clicking Build launches the build process. For any other platform, clicking Switch Platform makes that the active platform that Unity is currently dealing with.
警告:在大型项目中,切换平台通常需要相当长的时间才能完成;请确保您已准备好等待。这是因为 Unity 会以最佳方式为每个平台重新压缩所有资产(例如纹理)。
WARNING When in a big project, switching platforms often takes quite a bit of time to complete; make sure you’re ready to wait. This is because Unity recompresses all assets (such as textures) in an optimal way for each platform.
提示Build And Run 的功能与 Build 相同,此外它还会自动运行构建的应用程序。我通常想手动完成这一部分,因此很少使用 Build And Run。
TIP Build And Run does the same thing as Build, plus it automatically runs the built application. I usually want to do that part manually, so I rarely use Build And Run.
单击“构建”时,首先出现的是文件选择器,以便您可以告诉 Unity 在哪里生成应用包。选择文件位置后,构建过程就开始了。Unity 为当前活动的平台创建一个可执行的应用包。让我们来看看最流行的平台的构建过程:桌面、网络和移动。
When you click Build, the first thing that comes up is a file selector so that you can tell Unity where to generate the app package. Once you select a file location, the build process starts. Unity creates an executable app package for the currently active platform. Let’s go over the build process for the most popular platforms: desktop, web, and mobile.
这首次学习构建 Unity 游戏时,最简单的起点是部署到台式计算机 - Windows PC、macOS 或 Linux。由于 Unity 在台式计算机上运行,这意味着您将为正在使用的计算机构建应用程序。
The simplest place to start when first learning to build Unity games is by deploying to desktop computers—Windows PC, macOS, or Linux. Because Unity runs on desktop computers, that means you’ll build an app for the computer you’re already using.
注意:在本节中,您可以打开任何项目。说真的,任何 Unity 项目都可以。事实上,我强烈建议在每一节中使用不同的项目,以证明 Unity 可以为任何平台构建任何项目!
NOTE Open up any project to work with in this section. Seriously, any Unity project will work. In fact, I strongly suggest using a different project in every section to drive home the fact that Unity can build any project to any platform!
第一的选择“文件”>“构建设置”以打开“构建设置”窗口。默认情况下,当前平台将设置为 PC、Mac 和 Linux,但如果不是当前平台,请从列表中选择正确的平台,然后单击“切换平台”。
First choose File > Build Settings to open the Build Settings window. By default, the current platform will be set to PC, Mac, and Linux, but if that isn’t current, select the correct platform from the list and click Switch Platform.
在窗口的右侧,您会看到目标平台菜单。此菜单允许您在 Windows PC、macOS 和 Linux 之间进行选择。左侧列表中将这三个平台视为一个平台,但它们是截然不同的平台,因此请选择正确的平台。
On the right-hand side of the window, you’ll notice the Target Platform menu. This menu lets you choose between Windows PC, macOS, and Linux. All three are treated as one platform in the list on the left-hand side, but these are very different platforms, so choose the correct one.
选择桌面平台后,点击“构建”。如前所述,会弹出一个文件对话框,让您选择构建应用程序的存放位置。然后构建过程开始;对于大型项目,这可能需要一段时间,但对于您制作的小型演示,构建过程应该很快。
Once you’ve chosen your desktop platform, click Build. As explained previously, a file dialog pops up, allowing you to choose where the built application will go. Then the build process starts; this could take a while for a big project, but the build process should be fast for the tiny demos you’ve been making.
应用程序将出现在您选择的位置;双击它即可运行,就像任何其他程序一样。恭喜,这很简单!构建应用程序很简单,但可以通过多种方式自定义该过程;让我们看看如何调整建造。
The application will appear in the location you chose; double-click it to run it, like any other program. Congrats, that was easy! Building applications is a snap, but the process can be customized in various ways; let’s look at how to adjust the build.
提示:在 Windows 上使用 Alt-F4 或在 Mac 上使用 Cmd-Q 退出全屏游戏。完成的游戏应该有一个调用Application.Quit()的按钮。
TIP Quit full-screen games with Alt-F4 on Windows or Cmd-Q on Mac. Finished games should have a button that calls Application.Quit().
去返回到“构建设置”窗口,但这次单击播放器设置而不是构建。检查器中会出现一个巨大的设置列表(见图 13.2);这些设置控制构建应用程序的多个方面。
Go back to the Build Settings window, but this time click Player Settings instead of Build. A huge list of settings will appear in the Inspector (see figure 13.2); these settings control multiple aspects of the built application.
Figure 13.2 Player settings displayed in the Inspector
由于设置数量众多,您可能需要查阅 Unity 手册。相关文档页面为http://mng.bz/4Koa。
Because of the large number of settings, you’ll probably want to look them up in Unity’s manual. The relevant doc page is http://mng.bz/4Koa.
顶部的前几个设置最容易理解:公司名称、产品名称、版本和默认图标。输入前三个的值:公司名称是您的开发工作室的名称,产品名称是此特定游戏的名称,版本是随着游戏更新而增加的数字标识。然后从项目视图中拖动图像(如果需要,将图像导入项目)以将该图像设置为图标;构建应用程序时,此图像将显示为应用程序的图标。
The first several settings at the top are easiest to understand: Company Name, Product Name, Version, and Default Icon. Type in values for the first three: Company Name is the name for your development studio, Product Name is the name of this specific game, and Version is a number designation to increase as you update the game. Then drag an image from the Project view (import an image into the project if needed) to set that image as the icon; when the app is built, this image will appear as the application’s icon.
自定义应用程序的图标和名称对于使其外观完美非常重要。自定义构建应用程序行为的另一种有用方法是使用平台相关的代码。
Customizing the icon and name of the application is important for giving it a finished appearance. Another useful way of customizing the behavior of built applications is with platform-dependent code.
经过默认情况下,您编写的所有代码将在所有平台上以相同的方式运行。但 Unity 提供了编译器指令(称为平台定义),可使不同的代码在不同的平台上运行。您可以在手册http://mng.bz/Qq4w中找到平台定义的完整列表。
By default, all the code you write will run the same way on all platforms. But Unity provides compiler directives (known as platform defines) that cause different code to run on different platforms. You’ll find the full list of platform defines in the manual at http://mng.bz/Qq4w.
正如该页面所示,Unity 支持的每个平台都有指令,允许您在每个平台上运行单独的代码。通常,您的大部分代码不必位于平台指令内,但偶尔会有一小部分代码需要在不同平台上以不同的方式运行。例如,某些代码程序集仅存在于一个平台上,因此您需要在这些命令周围使用平台编译器指令。以下清单显示了如何编写此类代码。
As that page indicates, directives are available for every platform that Unity supports, allowing you to run separate code on every platform. Usually, the majority of your code doesn’t have to be inside platform directives, but occasionally small bits of the code need to run differently on different platforms. For example, some code assemblies exist on only one platform, so you need to have platform compiler directives around those commands. The following listing shows how to write such code.
清单 13.1 PlatformTest脚本展示了如何编写与平台相关的代码
Listing 13.1 PlatformTest script showing how to write platform-dependent code
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类平台测试:MonoBehaviour {
无效的OnGUI(){
#如果 UNITY_EDITOR ❶
GUI.Label(new Rect(10, 10, 200, 20), "在编辑器中运行");
#elif UNITY_STANDALONE ❷
GUI.Label(new Rect(10, 10, 200, 20), "在桌面上运行");
#别的
GUI.Label(new Rect(10, 10, 200, 20), "在其他平台上运行");
#结束
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlatformTest : MonoBehaviour {
void OnGUI() {
#if UNITY_EDITOR ❶
GUI.Label(new Rect(10, 10, 200, 20), "Running in Editor");
#elif UNITY_STANDALONE ❷
GUI.Label(new Rect(10, 10, 200, 20), "Running on Desktop");
#else
GUI.Label(new Rect(10, 10, 200, 20), "Running on other platform");
#endif
}
}
❶ This section runs only within the editor.
❷ Only in desktop/standalone applications
创建一个名为PlatformTest的脚本,并在其中写入此列表中的代码。将该脚本附加到场景中的对象(任何对象都可用于测试),屏幕左上角将出现一条小消息。当您在 Unity 的编辑器中玩游戏时,消息将显示正在编辑器中运行,但如果您构建游戏并运行构建的应用程序,消息将显示正在桌面上运行。在每种情况下都会运行不同的代码!
Create a script called PlatformTest and write the code from this listing in it. Attach that script to an object in the scene (any object will do for testing), and a small message will appear in the top-left of the screen. When you play the game within Unity’s editor, the message will say Running in Editor, but if you build the game and run the built application, the message will say Running on Desktop. Different code is being run in each case!
对于此测试,您使用了将所有桌面平台视为一个平台的平台定义,但如该文档页面所示,Windows、Mac 和 Linux 有单独的平台定义。事实上,Unity 支持的所有平台都有平台定义,因此您可以在每个平台上运行不同的代码。让我们继续讨论下一个重要平台:这网络。
For this test, you used the platform define that treats all desktop platforms as one, but as indicated on that doc page, separate platform defines are available for Windows, Mac, and Linux. In fact, there are platform defines for all the platforms supported by Unity so that you can run different code on each. Let’s move on to the next important platform: the web.
虽然桌面平台是 Unity 游戏最基本的目标平台,而 Web 则是 Unity 游戏的另一个重要平台。部署到 Web 上的游戏在 Web 浏览器中运行,因此可以通过互联网玩。
Although desktop platforms are the most basic targets to build for, another important platform for Unity games is the web. Games deployed to the web run within a web browser and can thus be played over the internet.
打开一个不同的项目(再次强调,任何项目都可以工作)并打开“构建设置”窗口。将平台切换到 WebGL,然后单击“构建”按钮。将出现一个文件选择器;输入此应用程序的名称WebTest,并根据需要更改为安全位置(不在 Unity 项目中的位置)。
Open a different project (again, this is to emphasize that any project will work) and open the Build Settings window. Switch the platform to WebGL and then click the Build button. A file selector will come up; type in the name WebTest for this application, and change to a safe location (a location not within the Unity project) if necessary.
构建过程现在将创建一个文件夹,其中包含一个 index.html 网页,以及包含所有游戏代码和其他资产的子文件夹。打开此网页,游戏应该嵌入在空白页面的中间。您需要从 Web 服务器运行游戏,而不是简单地将 index.html 作为本地文件打开。就像第 10 章一样,如果您已经有网站,则可以使用现有的 Web 服务器,或者您可以使用 XAMPP 之类的工具在 http://localhost/ 上进行测试。
The build process will now create a folder containing an index.html web page, as well as subfolders with all the game’s code and other assets. Open this web page, and the game should be embedded in the middle of the otherwise blank page. You will need to run the game from a web server, rather than simply opening index.html as a local file. Just as in chapter 10, you could use an existing web server if you already have a website, or you could test on http://localhost/ with something like XAMPP.
注意:您可能需要调整 Web 服务器的设置,以便正确处理 WebGL 构建中的压缩档案。Unity 手册 ( http://mng.bz/XreG ) 解释了这些服务器设置,但如果您出于某种原因无法调整这些设置(例如,游戏将位于您无法配置的第三方站点上),您也可以告诉 Unity 在构建中包含解压缩程序。在 WebGL 播放器设置的发布设置部分中打开解压缩程序回退。此设置默认关闭,因为浏览器的解压缩效果更好。但请注意,因为启用此设置后,您将不会注意到配置不正确的服务器。
NOTE You may need to adjust the settings of your web server for correct handling of compressed archives in the WebGL build. Unity’s manual (http://mng.bz/XreG) explains these server settings, but if you can’t adjust these for some reason (for example, the game will be on a third-party site that you cannot configure), you can also tell Unity to include a decompressor in the build. Turn on Decompressor Fallback in the Publishing Settings section of the WebGL player settings. This setting is off by default, because the browser’s decompression is better. Be warned though, because with this setting on, you won’t notice an improperly configured server.
这个网页没有什么特别之处;它只是一个用来测试游戏的示例。您可以自定义该页面上的代码,甚至可以提供您自己的网页(稍后讨论)。最重要的自定义之一是启用 Unity 和浏览器之间的通信,所以让我们来看一下下一个。
There’s nothing particularly special about this web page; it’s just an example to test your game with. It’s possible to customize the code on that page or even provide your own web page (discussed later). One of the most important customizations to make is enabling communication between Unity and the browser, so let’s go over that next.
一个Unity 网络游戏可以与浏览器(或者更确切地说是与浏览器中运行的 JavaScript)进行通信,并且这些消息可以双向传输:从 Unity 到浏览器,以及从浏览器到 Unity。要向浏览器发送消息,您需要将 JavaScript 代码写入代码库,然后 Unity 会提供特殊命令来使用该库中的函数。
A Unity web game can communicate with the browser (or rather with JavaScript running in the browser), and these messages can go in both directions: from Unity to the browser, and from the browser to Unity. To send messages to the browser, you write JavaScript code into a code library, and then Unity has special commands to use functions in that library.
同时,对于来自浏览器的消息,浏览器中的 JavaScript 会通过名称识别对象,然后 Unity 将消息传递给场景中命名的对象。因此,场景中必须有一个对象来接收来自浏览器的通信。
Meanwhile, for messages from the browser, JavaScript in the browser identifies an object by name, and then Unity passes the message to the named object in the scene. Thus, you must have an object in the scene that will receive communications from the browser.
为了演示这些任务,在 Unity 中创建一个名为WebTestObject的新脚本。同时在活动场景中创建一个名为JSListener的空对象(场景中的对象必须具有该确切名称,因为这是清单 13.4 中的 JavaScript 代码使用的名称)。将新脚本附加到该对象,然后写入此清单中的代码。
To demonstrate these tasks, create a new script in Unity called WebTestObject. Also create an empty object in the active scene called JSListener (the object in the scene must have that exact name, because that’s the name used by the JavaScript code in listing 13.4). Attach the new script to that object and then write in the code from this listing.
清单 13.2用于测试与浏览器通信的WebTestObject脚本
Listing 13.2 WebTestObject script for testing communication with the browser
使用System.Runtime.InteropServices;
使用 UnityEngine;
公共类 WebTestObject : MonoBehaviour {
私人字符串消息;
[DllImport(“__Internal”)] ❶
私有静态外部无效ShowAlert(字符串消息);
无效开始(){
message = "暂无消息";
}
无效更新(){
if (Input.GetMouseButtonDown(0)) { ❷
ShowAlert("大家好!");
}
}
无效的OnGUI(){
GUI.Label(new Rect(10,10,200,20),消息); ❸
}
public void RespondToBrowser(string message){ ❹
这个.消息=消息;
}
}using System.Runtime.InteropServices;
using UnityEngine;
public class WebTestObject : MonoBehaviour {
private string message;
[DllImport("__Internal")] ❶
private static extern void ShowAlert(string msg);
void Start() {
message = "No message yet";
}
void Update() {
if (Input.GetMouseButtonDown(0)) { ❷
ShowAlert("Hello out there!");
}
}
void OnGUI() {
GUI.Label(new Rect(10, 10, 200, 20), message); ❸
}
public void RespondToBrowser(string message) { ❹
this.message = message;
}
}
❶ Import the function from the JS library.
❷ On mouse click, call the imported function.
❸ Display the message in top left of the screen.
❹ Function for the browser to call
主要的新内容是DllImport命令。这将从 JavaScript 库中导入一个函数以在 C# 代码中使用。这显然意味着您有一个 JavaScript 库,因此接下来编写它。
The main new bit is the DllImport command. That imports a function from the JavaScript library to use in C# code. That obviously implies you have a JavaScript library, so write that next.
首先创建一个特殊的文件夹来包含它:创建一个名为Plugins的文件夹,然后在其中创建一个名为WebGL 的文件夹。现在将一个名为WebTest的文件放在WebGL 文件夹中,该文件的扩展名为 jslib (即 WebTest.jslib);最简单的方法是在 Unity 外部创建一个文本文件,重命名,然后将文件拖进去。Unity 会将该文件识别为 JavaScript 库,因此请在其中写入此代码。
First create the special folder to contain it: create a folder called Plugins, and within that create a folder called WebGL. Now put a file called WebTest that has the extension jslib (so WebTest.jslib) in the WebGL folder; the simplest way is to create a text file outside Unity, rename it, and then drag the file in. Unity will recognize that file as a JavaScript library, so write this code in it.
Listing 13.3 WebTest JavaScript library
合并到(LibraryManager.library,{
ShowAlert: 函数(msg){ ❶
窗口.alert(Pointer_stringify(msg));
},
});mergeInto(LibraryManager.library, {
ShowAlert: function(msg) { ❶
window.alert(Pointer_stringify(msg));
},
});
❶ The function imported and called from C#
jslib 文件包含一个包含函数的 JavaScript 对象和将自定义对象合并到 Unity 库管理器的命令。请注意,编写的函数除了标准 JavaScript 命令外,还包括Pointer_stringify();当从 Unity 传递字符串时,它会变成数字标识符,因此 Unity 提供了该函数来查找指向的字符串。
The jslib file contains both a JavaScript object containing functions and the command to merge the custom object into Unity’s library manager. Note that the function written includes Pointer_stringify() besides standard JavaScript commands; when passing a string from Unity, it’s turned into a numeric identifier, so Unity provides that function to look up the string pointed to.
现在再次构建 Web,查看新代码的实际效果。当您在网页的 Unity 游戏部分内单击时,Unity 中的WebTestObject会调用 JavaScript 代码中的函数;尝试单击几次,您会看到浏览器中出现一个警告框!
Now build for the web again to see the new code in action. The WebTestObject in Unity calls a function in the JavaScript code when you click within the Unity game part of the web page; try clicking a few times, and you’ll see an alert box appear in the browser!
注意Unity 还具有Application.ExternalEval()用于在浏览器中运行代码;ExternalEval运行任意 JavaScript 代码片段,而不是调用已定义的函数。此方法已弃用,应避免使用,但有时它的简单性很有用,例如仅使用Application.ExternalEval("location.reload();")即可重新加载页面。
NOTE Unity also has Application.ExternalEval() for running code in the browser; ExternalEval runs arbitrary snippets of JavaScript, rather than calling defined functions. This method is deprecated and should be avoided, but sometimes its simplicity is useful, like reloading the page with just Application.ExternalEval("location.reload();").
好了,您已经测试了 Unity 游戏与网页中的 JavaScript 之间的通信,但网页也可以向 Unity 发送消息,所以我们也来做一下。这将涉及页面上的新代码和按钮;幸运的是,Unity 提供了一种自定义网页的简单方法。具体来说,Unity 在构建到 WebGL 时会填充网页模板,您可以选择自定义模板而不是默认模板。
All right, you have tested communication from the Unity game to JavaScript in the web page, but the web page can also send a message back to Unity, so let’s do that too. This will involve new code and buttons on the page; fortunately, Unity provides an easy way to customize the web page. Specifically, Unity fills in a web page template when it builds to WebGL, and you can choose a custom template instead of the default one.
默认模板可以在 Unity 安装文件夹(Windows 上通常为 C:\Program Files\Unity\Editor\Data,Mac 上为 /Applications/Unity/Editor)的 /WebGLSupport/BuildTools/WebGLTemplates 下找到。在文本编辑器中打开模板页面,您会看到模板主要是标准 HTML 和 JavaScript,以及 Unity 用生成的信息替换的一些特殊标签。虽然最好不要使用 Unity 的内置模板,但它们(尤其是最小模板)是构建您自己的模板的良好基础。您将把最小模板网页复制到您制作的自定义模板中。
The default templates can be found in the Unity installation folder (usually C:\Program Files\Unity\Editor\Data on Windows, or /Applications/Unity/Editor on Mac) under /WebGLSupport/BuildTools/WebGLTemplates. Open a template page in a text editor and you’ll see that a template is largely standard HTML and JavaScript, plus some special tags that Unity replaces with generated information. Although it’s best for you to leave Unity’s built-in templates alone, they (especially the minimal one) make a good base on which to build your own. You’ll copy the minimal template web page into the custom template you make.
在 Unity 的 Project 视图中,在 Assets 下直接创建一个名为WebGLTemplates(无空格)的文件夹;自定义模板就放在这里。现在在其中创建一个名为WebTest的子文件夹;该文件夹用于存放您的新模板。将一个 index.html 文件放在这里(您可以从最小模板中复制网页),在文本编辑器中打开它,然后在其中写入此代码。
In Unity’s Project view, create a folder called WebGLTemplates (no space) directly under Assets; that’s where custom templates go. Now create a subfolder within it named WebTest; that folder is for your new template. Put an index.html file in here (you can copy in the web page from the minimal template), open that in a text editor, and write this code in it.
清单 13.4 用于实现浏览器与 Unity 通信的 WebGL 模板
Listing 13.4 WebGL template to enable browser-Unity communication
<!DOCTYPE html>
<html lang="en-us">
<头部>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Unity WebGL 播放器 | {{{ PRODUCT_NAME }}}} {{title>
<style>body { background-color: #333; } ❶
</head>
<body 样式="文本对齐:中心">
<canvas id="unity-canvas" 宽度={{{ WIDTH }}} 高度={{{ HEIGHT }}} style="width: {{{ WIDTH }}}px; 高度:{{{ HEIGHT }}}px; 背景:{{{ BACKGROUND_FILENAME ? 'url(\'Build/' + BACKGROUND_FILENAME.replace(/'/g, '%27') + '\') center / cover' : BACKGROUND_COLOR }}}"></canvas>
<br><input type="button" value="发送到 Unity" onclick="SendToUnity();" /> ❷
<script src="构建/{{{ LOADER_FILENAME }}}"></script>
<脚本>
var unityInstance = null;
创建UnityInstance(document.querySelector(“#unity-canvas”),{
dataUrl: "构建/{{{ DATA_FILENAME }}}",
frameworkUrl: "构建/{{{ FRAMEWORK_FILENAME }}}",
codeUrl: "构建/{{{ CODE_FILENAME }}}",
#如果内存文件名
memoryUrl: "构建/{{{ MEMORY_FILENAME }}}",
#结束
#如果符号文件名
symbolUrl: "构建/{{{ SYMBOLS_FILENAME }}}",
#结束
streamingAssetsUrl:“流资产”,
公司名称:“{{{ COMPANY_NAME }}}”,
产品名称:“{{{ PRODUCT_NAME }}}”,
产品版本:“{{{ PRODUCT_VERSION }}}”,
}).then((createdInstance) => {
unity实例 = 创建实例;
});
函数SendToUnity(){
unityInstance.SendMessage("JSListener", ❸
“RespondToBrowser”,“来自浏览器的问候!”);
}
</script>
</body>
</html><!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Unity WebGL Player | {{{ PRODUCT_NAME }}}</title>
<style>body { background-color: #333; }</style> ❶
</head>
<body style="text-align: center">
<canvas id="unity-canvas" width={{{ WIDTH }}} height={{{ HEIGHT }}} style="width: {{{ WIDTH }}}px; height: {{{ HEIGHT }}}px; background: {{{ BACKGROUND_FILENAME ? 'url(\'Build/' + BACKGROUND_FILENAME.replace(/'/g, '%27') + '\') center / cover' : BACKGROUND_COLOR }}}"></canvas>
<br><input type="button" value="Send to Unity" onclick="SendToUnity();" /> ❷
<script src="Build/{{{ LOADER_FILENAME }}}"></script>
<script>
var unityInstance = null;
createUnityInstance(document.querySelector("#unity-canvas"), {
dataUrl: "Build/{{{ DATA_FILENAME }}}",
frameworkUrl: "Build/{{{ FRAMEWORK_FILENAME }}}",
codeUrl: "Build/{{{ CODE_FILENAME }}}",
#if MEMORY_FILENAME
memoryUrl: "Build/{{{ MEMORY_FILENAME }}}",
#endif
#if SYMBOLS_FILENAME
symbolsUrl: "Build/{{{ SYMBOLS_FILENAME }}}",
#endif
streamingAssetsUrl: "StreamingAssets",
companyName: "{{{ COMPANY_NAME }}}",
productName: "{{{ PRODUCT_NAME }}}",
productVersion: "{{{ PRODUCT_VERSION }}}",
}).then((createdInstance) => {
unityInstance = createdInstance;
});
function SendToUnity() {
unityInstance.SendMessage("JSListener", ❸
"RespondToBrowser", "Hello from the browser!");
}
</script>
</body>
</html>
❶ Making the page dark instead of white
❷ Button that calls the JavaScript function
❸ SendMessage() 指向Unity中的命名对象。
❸ SendMessage() points to the named object in Unity.
如果您复制了最小模板,您将看到清单 13.4 只是在那里添加了几行。两个重要的添加是脚本标记中的函数和页面上的输入按钮;添加的样式会更改页面的颜色,以便更容易看到嵌入的游戏。按钮的 HTML 标记链接到 JavaScript 函数,该函数在 Unity 实例上调用SendMessage()。此方法在 Unity 中的命名对象上调用一个函数;第一个参数是对象的名称,第二个参数是方法的名称,第三个参数是调用方法时要传入的字符串。
If you copied the minimal template, you’ll see that listing 13.4 simply adds a few lines there. The two important additions are a function in the script tag and an input button on the page; the added style changes the color of the page to make it easier to see the embedded game. The button’s HTML tag links to a JavaScript function, and that function calls SendMessage() on the Unity instance. This method calls a function on a named object within Unity; the first parameter is the name of the object, the second parameter is the name of the method, and the third parameter is a string to pass in while calling the method.
您已经创建了自定义模板,但仍需要告诉 Unity 使用此模板而不是默认模板。再次打开“播放器设置”(记住,在“构建设置”窗口中单击“播放器设置”),然后在 Web 设置中找到“WebGL 模板”(如图 13.3 所示)。您会看到当前选择了“默认”,但 WebTest(您创建的模板文件夹)也在列表中;单击该模板。
You’ve made your custom template, but you still have to tell Unity to use this template instead of the default. Open the Player Settings again (remember, click Player Settings in the Build Settings window) and find WebGL Template in the web settings (shown in figure 13.3). You’ll see that Default is currently selected, but WebTest (the template folder you created) is also on the list; click that one instead.
Figure 13.3 WebGL Template setting
选择自定义模板后,再次构建到 WebGL。打开生成的网页,这次页面底部有一个按钮。单击该按钮,您将看到 Unity 中显示的更改消息!
With the custom template selected, build to WebGL again. Open the generated web page, and this time a button is at the bottom of the page. Click the button and you’ll see the changed message displayed in Unity!
以上就是关于 Web 构建的浏览器通信。接下来是构建的下一个重要平台(或者说一组平台)应用程序:移动的。
That wraps up browser communication for web builds. On to the next important platform (or rather, set of platforms) for building apps: mobile.
移动的应用程序是 Unity 的另一个重要构建目标。我的直觉(完全不科学)是,使用 Unity 创建的大多数商业游戏都是手机游戏。
Mobile apps are another important build target for Unity. My gut impression (totally unscientific) is that most commercial games created using Unity are mobile games.
定义 移动是手持计算设备的一种。该名称最初指智能手机,但现在包括平板电脑。最广泛使用的两种移动计算平台是 iOS(来自 Apple)和 Android(来自 Google)。
DEFINITION Mobile is a category of handheld computing devices. The designation started with smartphones but now includes tablets. The two most widely used mobile computing platforms are iOS (from Apple) and Android (from Google).
设置移动应用程序的构建过程比桌面或 Web 构建更复杂,因此这是另一个可选部分 — 可选的意思是只阅读它,而不实际遵循步骤。我仍然会假设您正在努力,但您必须购买 iOS 开发者许可证并安装 Android 的所有开发者工具。
Setting up the build process for mobile apps is more complicated than either desktop or web builds, so this is another optional section—optional as in only read through it, without actually following the steps. I’ll still write as if you’re working along, but you’d have to buy a developer license for iOS and install all the developer tools for Android.
警告移动设备经历了如此快速的变化,以至于当您阅读本文时,确切的构建过程可能会略有不同。高级概念可能仍然适用,但您应该查看最新的在线文档,以准确了解要执行的命令和要按下的按钮。对于初学者,以下是 Apple ( https://developer.apple.com/documentation/xcode ) 和 Google ( https://developer.android.com/studio/build ) 的文档页面。
WARNING Mobile devices undergo so much rapid change that the exact build process is likely to be slightly different by the time you read this. The high-level concepts are probably still true, but you should look at up-to-date documentation online for an exact rundown of the commands to execute and buttons to push. For starters, here are the doc pages from Apple (https://developer .apple.com/documentation/xcode) and Google (https://developer.android .com/studio/build).
好了,说完这些注意事项,我将解释 iOS 和 Android 的整体构建过程。请记住,这些平台偶尔会更改构建过程的细节。
All right, with those caveats out of the way, I’ll explain the overall build process for both iOS and Android. Keep in mind that these platforms occasionally change the details of the build process.
移动的所有设备都与您正在开发的计算机是分开的,这种分离使得构建和部署到设备的过程稍微复杂一些。您需要设置各种专用工具,然后才能单击“构建”。
Mobile devices are all separate from the computer you’re developing on, and that separateness makes the process of building and deploying to devices slightly more complex. You’ll need to set up a variety of specialized tools before you can click Build.
在从高层次上讲,在 iOS 上部署 Unity 游戏的过程需要首先从 Unity 构建 Xcode 项目,然后将 Xcode 项目构建到 iOS 应用程序包中(IPA) 使用 Xcode。Unity 无法直接构建最终的 IPA,因为所有 iOS 应用都必须通过 Apple 的构建工具。这意味着您需要安装 Xcode(Apple 的编程 IDE),包括 iOS SDK。
At a high level, the process of deploying a Unity game on iOS requires first building an Xcode project from Unity and then building the Xcode project into an iOS application package (IPA) using Xcode. Unity can’t build the final IPA directly because all iOS apps have to go through Apple’s build tools. That means you need to install Xcode (Apple’s programming IDE), including the iOS SDK.
警告:部署 iOS 游戏时,您必须在 Mac 上工作 — Xcode 仅在 macOS 上运行。在 Unity 中开发游戏可以在 Windows 或 Mac 上进行,但构建 iOS 应用必须在 Mac 上进行。
WARNING You have to be working on a Mac when deploying an iOS game—Xcode runs only on macOS. Developing a game within Unity can be done on either Windows or Mac, but building the iOS app must be done on a Mac.
从 Apple 网站的开发者部分获取 Xcode:https: //developer.apple.com/xcode/。
Get Xcode from Apple’s website, in the developer section: https://developer.apple.com/xcode/.
注意:您需要成为 Apple 开发者计划的会员才能在 App Store 上销售您的 iOS 游戏。Apple 开发者计划的费用为每年 99 美元;请登录https://developer.apple.com/programs/进行注册。
NOTE You need membership in the Apple Developer Program in order to sell your iOS game on the App Store. Apple’s developer program costs $99/year; enroll at https://developer.apple.com/programs/.
安装 Xcode 后,启动它并打开“偏好设置”以添加您的开发者帐户。当 Xcode 在构建应用程序时访问您的帐户时,您需要登录。
Once Xcode is installed, launch it and open Preferences to add your developer account. You need to be logged in when Xcode accesses your account while building an app.
现在返回 Unity 并切换到 iOS。您需要调整 iOS 应用的 Player 设置(记住,打开 Build Settings 并单击 Player Settings)。您应该已经位于 Player 设置的 iOS 选项卡上,但如果需要,请单击带有 iOS 图标的选项卡。向下滚动到 Other Settings,然后查找 Identification。需要调整 Bundle Identifier,以便 Apple 能够正确识别该应用。
Now go back to Unity and switch to iOS. You need to adjust the Player settings for the iOS app (remember, open Build Settings and click Player Settings). You should already be on the iOS tab of the Player settings, but click the tab with an iOS icon if needed. Scroll down to Other Settings and then look for Identification. Bundle Identifier needs to be adjusted so that Apple will correctly identify the app.
注意iOS 将其称为 Bundle Identifier,Android 将其称为 Package Name,但其他命名方式在两个平台上都相同。标识符应遵循与任何代码包相同的约定:全部小写,格式为com.companyname.productname。
NOTE iOS calls it Bundle Identifier, and Android calls it Package Name, but naming otherwise works the same way on both platforms. The identifier should follow the same convention as that for any code package: all lowercase in the form com.companyname.productname.
另一个适用于 iOS 和 Android 的重要设置是版本(这是应用程序的版本号)。除此之外的大多数设置都是特定于平台的;例如,iOS 添加了一个额外的内部版本号,与主版本号分开。还有一个脚本后端设置;过去一直使用 Mono,但较新的 IL2CPP 后端支持 iOS 更新,例如 64 位二进制文件。
Another important setting that applies to both iOS and Android is Version (this is the version number of the app). Most of the settings beyond that are platform-specific; for example, iOS added an additional build number, separate from the main version number. There’s also a setting for Scripting Backend; Mono was always used in the past, but the newer IL2CPP backend supports iOS updates, like 64-bit binaries.
注意: Unity 的 iOS 构建版本无法同时在真实设备(iPhone 和 iPad)和 iOS 模拟器上运行。默认情况下,Unity 的 iOS 构建版本仅适用于真实设备,但您可以通过在播放器设置中向下滚动到目标 SDK 切换到模拟器构建版本。实际上,我从未这样做过,因为我所有的“真实设备外测试”工作都是在 Unity 本身内完成的,如果我要构建 iOS 版本,那么我想在实际手机上运行它。
NOTE iOS builds from Unity don’t work with both real devices (iPhones and iPads) and iOS simulators. By default, iOS builds from Unity work only on real devices, but you can switch to building for simulators by scrolling down to Target SDK in Player settings. In practice, I’ve never had to do this, because all my “testing outside real device” work is done within Unity itself, and if I’m doing an iOS build, then I want to run it on an actual phone.
现在单击“Build Settings”窗口中的“Build”。选择构建文件的位置,这将在该位置生成一个 Xcode 项目;您可能希望单击按钮以创建一个新文件夹,然后选择该新创建的文件夹。
Now click Build in the Build Settings window. Select the location for the built files, and that’ll generate an Xcode project in that location; you probably want to click the button to create a new folder and then choose that newly created folder.
如果需要,可以直接修改生成的 Xcode 项目(简单的修改可以是构建后脚本的一部分)。无论如何,打开 Xcode 项目;构建的文件夹有许多文件,但双击 .xcodeproj 文件(它有一个蓝图图标)。Xcode 将打开并加载此项目。Unity 已经处理了项目中所需的大部分设置,但您需要调整正在使用的配置文件。
The Xcode project that results can be modified directly if you want (simple modifications could be part of the post-build script). Regardless, open the Xcode project; the built folder has many files, but double-click the .xcodeproj file (it has an icon of a blueprint). Xcode will open with this project loaded. Unity already took care of most of the settings needed in the project, but you do need to adjust the provisioning profiles being used.
Xcode 会尝试自动设置签名配置文件,这就是您之前在“偏好设置”中添加帐户的原因。在 Xcode 左侧的项目列表中选择您的应用,将出现几个与所选项目相关的选项卡。单击“签名和功能”选项卡,然后单击“团队”菜单以选择在 Apple 开发者计划中注册的团队(见图 13.4)。如果出于某种原因您不希望 Xcode 自动管理签名,可以通过向下滚动到“构建设置”选项卡中的“签名”来手动调整配置文件。
Xcode will attempt to set up the signing profiles automatically, so this is why you added your account in Preferences earlier. Select your app in the project list on the left-hand side of Xcode, and several tabs relevant to the selected project will appear. Click the tab for Signing & Capabilities and click the Team menu to select the team registered with Apple’s developer program (see figure 13.4). If for some reason you don’t want Xcode to automatically manage signing, provisioning profiles can be adjusted manually by scrolling down to Signing in the Build Settings tab.
Figure 13.4 Provisioning/signing settings in Xcode
设置配置文件后,您就可以构建应用程序了。从“产品”菜单中,选择“运行”或“存档”。“产品”菜单有很多选项,包括名称诱人的“构建”,但就我们的目的而言,有用的两个选项是“运行”和“存档”。构建会生成可执行文件,但不会将它们打包到 iOS 中,而“运行”和“存档”的作用如下:
Once the provisioning profiles are set, you’re ready to build the app. From the Product menu, choose either Run or Archive. The Product menu has a lot of options, including the tantalizingly named Build, but for our purposes, the two options that are useful are Run and Archive. Build generates executable files but doesn’t bundle them for iOS, whereas this is what Run and Archive do:
Run will test the application on an iPhone connected to the computer with a USB cable.
Archive will create an application package that can be sent to other registered devices (either for release, or testing via what Apple refers to as ad hoc distribution).
Archive 不会直接创建应用程序包,而是在原始代码文件和 IPA 之间的中间阶段创建一个包。创建的存档将列在 Xcode 的 Organizer 窗口中;在该窗口中,选择生成的存档并点击右侧的 Distribute App。点击后,系统会询问您是否要在商店中分发应用程序或临时分发应用程序。
Archive doesn’t create the app package directly but rather creates a bundle in an intermediate stage between the raw code files and an IPA. The created archive will be listed in Xcode’s Organizer window; in that window, select the generated archive and click Distribute App on the right-hand side. After you click that, you’ll be asked if you want to distribute the app on the store or ad hoc.
如果您选择临时分发,您将最终获得一个可以发送给测试人员的 IPA 文件。您可以直接发送文件让他们通过 iTunes 安装,但建立一个网站来处理测试版本的分发和安装会更方便。或者,对已上传到商店但尚未提交的版本使用 TestFlight ( https://developer.apple.com/testflight/ )然而。
If you choose ad hoc distribution, you’ll end up with an IPA file that can be sent to testers. You could send the file directly for them to install through iTunes, but it’s more convenient to set up a website to handle distributing and installing test builds. Alternatively, use TestFlight (https://developer.apple.com/testflight/) on builds that have been uploaded to the store but not submitted yet.
Setting up Android build tools
不同于iOS 应用程序,Unity 可以直接生成最终的 Android 应用程序(APK,用于 Android 应用程序包,或 AAB,用于 Android 应用程序包)。这需要将 Unity 指向包含必要编译器的 Android SDK。您可以安装 Android SDK 以及 Unity 的 Android 构建模块,也可以从 Android Studio 中安装它并在 Unity 的偏好设置中指向该文件位置(见图 13.5)。您可以从https://developer.android.com/studio下载 Android 构建工具。
Unlike iOS apps, Unity can generate the final Android application (either an APK, for Android application package, or AAB, for Android app bundle) directly. This requires pointing Unity to the Android SDK, which includes the necessary compiler. You could install the Android SDK along with the Android build module for Unity, or you could install it from within Android Studio and point to that file location in Unity’s preferences (see figure 13.5). You can download the Android build tools from https://developer.android.com/studio.
图 13.5 Unity 偏好设置指向 Android SDK
Figure 13.5 Unity preference setting to point to Android SDK
在 Unity 的偏好设置中设置 Android SDK 后,您需要像为 iOS 指定应用程序的标识符一样指定应用程序的标识符。您会在播放器设置中找到包名称;将其设置为com.companyname.productname(如之前为 iOS 设置包标识符时所述)。然后单击构建以开始该过程。与所有构建一样,Unity 将首先询问将文件保存在哪里。然后它将在该位置创建一个 APK 文件。
After setting the Android SDK in Unity’s preferences, you need to specify the app’s identifier just as you did for iOS. You’ll find Package Name in Player Settings; set it to com.companyname.productname (as explained previously when setting the Bundle Identifier for iOS). Then click Build to start the process. As with all builds, Unity will first ask where to save the file. Then it’ll create an APK file in that location.
现在您有了应用程序包,您必须将其安装在设备上。您可以通过从网络下载文件(Google Drive 等云存储可用于此目的)或通过连接到计算机的 USB 电缆传输文件(这种方法称为侧载)将 APK 文件放到 Android 手机上。通过 USB 传输文件的细节因设备而异,但一旦到达那里,就可以使用文件管理器应用程序安装文件。我不知道为什么文件管理器没有内置在 Android 中,但你可以从 Google Play 商店免费安装一个。在文件管理器中导航到您的 APK 文件,然后安装应用程序。
Now that you have the app package, you must install it on a device. You can get the APK file onto an Android phone by downloading the file from the web (cloud storage like Google Drive is useful for this purpose) or by transferring the file via a USB cable connected to your computer (an approach referred to as sideloading). The details of how to transfer files via USB vary for every device, but once there, the files can be installed using a file manager app. I don’t know why file managers aren’t built into Android, but you can install one for free from the Google Play Store. Navigate to your APK file within the file manager and then install the app.
如您所见,Android 的基本构建过程比 iOS 的构建过程简单得多。遗憾的是,自定义构建和实现插件的过程比 iOS 更复杂;稍后您将了解具体方法。但在此之前,让我们先讨论一下质地压缩。
As you can see, the basic build process for Android is a lot simpler than the build process for iOS. Unfortunately, the process of customizing the build and implementing plugins is more complicated than with iOS; you’ll learn how in a bit. But before that, let’s talk about texture compression.
资产会占用大量内存,纹理尤其如此。为了减小文件大小,您可以采用各种方法来压缩资源,每种方法都有优缺点。由于这些优缺点,您可能需要调整 Unity 压缩纹理的方式。
Assets can eat up a lot of memory, and this especially includes textures. To reduce their file size, you can compress assets in various ways, with pros and cons to each method. Because of these pros and cons, you may need to adjust how Unity compresses textures.
管理移动设备上的纹理压缩至关重要,尽管从技术上讲,纹理在其他平台上也经常被压缩。但是,出于各种原因,您不必在其他平台上过多关注压缩——主要原因是该平台在技术上更成熟。在移动设备上,您需要更加关注纹理压缩,因为设备对这个细节更加敏感。
Managing texture compression on mobile devices is essential, though technically, textures are often compressed on other platforms too. But you don’t have to pay as much attention to compression on other platforms for various reasons—the chief one being that the platform is more technologically mature. On mobile devices, you need to pay closer attention to texture compression because the devices are touchier about this detail.
Unity 会自动为您压缩纹理。在大多数开发工具中,您需要自己压缩图像,但在 Unity 中,您通常会导入未压缩的图像,然后它会在图像的导入设置中应用图像压缩(见图 13.6)。
Unity automatically compresses textures for you. In most development tools, you need to compress images yourself, but in Unity, you generally import uncompressed images, and then it applies image compression in the import settings for the image (see figure 13.6).
Figure 13.6 Texture compression settings in the Inspector
不同平台上的压缩设置不同,因此当您切换平台时,Unity 会重新压缩图像。最初,设置是默认值,您可能需要针对特定图像和特定平台进行调整。特别是,图像压缩在 Android 上更加棘手。这主要是由于 Android 设备的碎片化:由于所有 iOS 设备都使用几乎相同的视频硬件,因此 iOS 应用可以针对其图形芯片 (GPU) 优化纹理压缩。Android 应用不享受相同的硬件统一性,因此它们的纹理压缩必须以最低公分母为目标。
The compression settings are different on different platforms, so Unity recompresses images when you switch platforms. Initially, the settings are default values, and you may need to adjust them for specific images and specific platforms. In particular, image compression is trickier on Android. This is mostly due to the fragmentation of Android devices: because all iOS devices use pretty much the same video hardware, iOS apps can have texture compression optimized for their graphics chips (the GPU). Android apps don’t enjoy the same uniformity of hardware, so their texture compression has to aim for the lowest common denominator.
更具体地说,所有 iOS 设备都使用(或者说曾经使用,并且仍然保持兼容)PowerVR GPU。因此,iOS 应用可以使用优化的 PowerVR 纹理压缩所有 iOS 设备上都支持 PVRTC,甚至支持自版本 6 以来所有 iPhone 上都支持的较新的 ASTC 格式。一些 Android 设备也使用 PowerVR 芯片,但它们也经常使用高通的 Adreno 芯片、ARM 的 Mali GPU 或其他选项。因此,Android 应用通常依赖于爱立信纹理压缩(ETC),一种更通用的压缩算法,所有 Android 设备都支持。Unity 默认使用 ETC2(更高级的第二个版本)来处理带有 alpha 通道的纹理,因为原始的 ETC 压缩格式没有 alpha 通道,但请注意,较旧的 Android 设备可能不支持 ETC2。
To be more specific, all iOS devices use (or rather used to use, and still maintain compatibility with) PowerVR GPUs. Thus, iOS apps can use optimized PowerVR Texture Compression (PVRTC) on all iOS devices, or even the newer ASTC format that is supported on all iPhones since version 6. Some Android devices also use PowerVR chips, but they just as frequently use Adreno chips from Qualcomm, Mali GPUs from ARM, or other options. As a result, Android apps generally rely on Ericsson Texture Compression (ETC), a more generic compression algorithm supported by all Android devices. Unity defaults to ETC2 (the more advanced second version) for textures with an alpha channel, since the original ETC compression format doesn’t have an alpha channel, but note that older Android devices may not support ETC2.
这种默认设置在大多数情况下都运行良好,但如果您需要调整纹理的压缩,请调整图 13.6 中所示的设置。单击 Android 图标选项卡以覆盖该平台的默认设置,然后使用“格式”菜单选择特定的压缩格式。特别是,您可能会发现某些关键图像需要解压缩;虽然它们的文件大小会大得多,但图像质量会更好。只要您压缩大多数纹理并仅根据具体情况选择未压缩,文件大小的增加可能不会太糟糕。讨论完毕后,移动开发的最后一个主题是开发原生插件。
This default works fairly well most of the time, but if you need to adjust compression on a texture, adjust the settings shown in figure 13.6. Click the Android icon tab to override the default settings for that platform, and then use the Format menu to pick specific compression formats. In particular, you may find that certain key images need to be uncompressed; although their file size will be much larger, the image quality will be better. As long as you compress the majority of textures and choose uncompressed only on a case-by-case basis, the increased file size probably won’t be too bad. With that discussion out of the way, the final topic for mobile development is developing native plugins.
统一内置了大量功能,但这些功能大多限于所有平台通用的功能。要利用特定于平台的工具包(例如 Android 上的 Play 游戏服务),通常需要 Unity 的附加插件。
Unity has a huge amount of functionality built in, but that functionality is mostly limited to features common across all platforms. Taking advantage of platform-specific toolkits (such as Play Game Services on Android) often requires add-on plugins for Unity.
提示有多种预制移动插件可用于 iOS 和 Android 特定功能;附录 D 列出了一些获取移动插件的地方。这些插件的运行方式与此处所述相同,但插件代码已为您编写。
TIP A variety of premade mobile plugins are available for iOS- and Android-specific features; appendix D lists a few places to get mobile plugins. These plugins operate in the manner described here, except that the plugin code is already written for you.
与移动插件通信的过程与与浏览器通信的过程类似。在 Unity 方面,特殊命令会调用插件内的函数。在插件方面,插件可以使用SendMessage()向 Unity 场景中的对象发送消息。确切的代码在不同平台上看起来有所不同,但总体思路始终相同。
The process of communicating with mobile plugins is similar to the process of communicating with the browser. On the Unity side of things, special commands call functions within the plugin. On the plugin’s side, the plugin can use SendMessage()to send a message to an object in Unity’s scene. The exact code looks different on different platforms, but the general idea is always the same.
警告:与初始构建过程一样,移动设备上的原生开发过程也经常发生变化 — 变化的不是 Unity 端,而是原生代码部分。我将从高层次介绍这些内容,但您应该在网上查找最新的文档。
WARNING Just as with the initial build process, the process for native development on mobile tends to change frequently—not so much the Unity end of the process, but the native code part. I’ll cover things at a high level, but you should look for up-to-date documentation online.
两个平台的插件都放在 Unity 中的同一个位置。如果需要,请在 Project 视图中创建一个名为Plugins的文件夹;然后在 Plugins 中为 Android 和 iOS 分别创建一个文件夹。将插件文件放入 Unity 后,它们还会具有适用于其平台的设置。通常,Unity 会自动计算出这一点(iOS 插件设置为 iOS,Android 插件设置为 Android,依此类推),但如果有必要,请在 Inspector 中查找这些设置。
Plugins for both platforms are put in the same place within Unity. If needed, create a folder in the Project view called Plugins; then, inside Plugins create a folder each for Android and iOS. Once they’re put into Unity, plugin files also have settings for the platforms they apply to. Normally, Unity figures this out automatically (iOS plugins are set to iOS, Android plugins are set to Android, and so on), but if necessary, look for these settings in the Inspector.
这插件实际上只是 Unity 调用的一些本机代码。首先,在 Unity 中创建一个脚本来处理本机代码;将此脚本命名为TestPlugin(参见下一个清单)。
The plugin is really just some native code that gets called by Unity. First, create a script in Unity to handle the native code; call this script TestPlugin (see the next listing).
清单 13.5从 Unity 调用 iOS 原生代码的TestPlugin脚本
Listing 13.5 TestPlugin script that calls iOS native code from Unity
使用系统;
使用System.Collections;
使用System.Runtime.InteropServices;
使用 UnityEngine;
公共类 TestPlugin : MonoBehaviour {
私有静态测试插件_实例;
公共静态void Initialize(){ ❶
如果 (_instance != null) {
Debug.Log("已找到TestPlugin实例。已初始化");
返回;
}
Debug.Log("未找到TestPlugin实例。正在初始化...");
GameObject 所有者 = 新 GameObject ("TestPlugin_instance");
_instance = 所有者.添加组件<TestPlugin>();
不要在加载时销毁(_instance);
}
#region iOS ❷
[DllImport("__Internal")] ❸
private static extern float _TestNumber(); ❸
[DllImport(“__Internal”)]
私有静态外部字符串_TestString(字符串测试);
#endregion iOS
公共静态浮点测试编号() {
浮点数val = 0f;
如果(应用程序.平台==运行时平台.IPhonePlayer)
val = _TestNumber(); ❹
返回值;
}
公共静态字符串 TestString(字符串测试){
字符串val =“”;
如果(应用程序.平台==运行时平台.IPhonePlayer)
val = _TestString(测试);
返回值;
}
}using System;
using System.Collections;
using System.Runtime.InteropServices;
using UnityEngine;
public class TestPlugin : MonoBehaviour {
private static TestPlugin _instance;
public static void Initialize() { ❶
if (_instance != null) {
Debug.Log("TestPlugin instance was found. Already initialized");
return;
}
Debug.Log("TestPlugin instance not found. Initializing...");
GameObject owner = new GameObject("TestPlugin_instance");
_instance = owner.AddComponent<TestPlugin>();
DontDestroyOnLoad(_instance);
}
#region iOS ❷
[DllImport("__Internal")] ❸
private static extern float _TestNumber(); ❸
[DllImport("__Internal")]
private static extern string _TestString(string test);
#endregion iOS
public static float TestNumber() {
float val = 0f;
if (Application.platform == RuntimePlatform.IPhonePlayer)
val = _TestNumber(); ❹
return val;
}
public static string TestString(string test) {
string val = "";
if (Application.platform == RuntimePlatform.IPhonePlayer)
val = _TestString(test);
return val;
}
}
❶该对象是在这个静态函数中创建的,因此不必在编辑器中创建它。
❶ The object is created in this static function, so you don’t have to create it in the editor.
❷ Tag that identifies section of code; the tag doesn’t do anything by itself.
❸ Refer to the function in the iOS code.
❹ Call this if the platform is IPhonePlayer.
首先,请注意静态Initialize()函数在场景中创建一个永久对象,这样您就不必在编辑器中手动执行此操作。您之前没有看到从头开始创建对象的代码,因为在大多数情况下使用预制件要简单得多,但在这种情况下,在代码中创建对象更简洁(这样您就可以使用插件脚本而无需编辑场景)。
First, note that the static Initialize() function creates a permanent object in the scene so that you don’t have to do it manually in the editor. You haven’t previously seen code to create an object from scratch because using a prefab is a lot simpler in most cases, but in this case, it’s cleaner to create the object in code (so that you can use the plugin script without editing the scene).
这里的主要魔法涉及DllImport和static extern命令。这些命令告诉 Unity 链接到您提供的本机代码中的函数。然后您可以在此脚本的方法中使用这些引用的函数(并检查以确保代码在 iPhone/iOS 上运行)。
The main wizardry going on here involves the DllImport and static extern commands. Those commands tell Unity to link up to functions in the native code you provide. Then you can use those referenced functions in this script’s methods (with a check to make sure the code is running on iPhone/iOS).
接下来,您将使用这些插件函数进行测试。创建一个名为MobileTestObject的新脚本,在场景中创建一个空对象,然后将脚本附加到该对象。
Next, you’ll use these plugin functions to test them. Create a new script called MobileTestObject, create an empty object in the scene, and then attach the script to the object.
清单 13.6 使用来自MobileTestObject的插件
Listing 13.6 Using the plugin from MobileTestObject
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
公共类 MobileTestObject : MonoBehaviour {
私人字符串消息;
无效唤醒(){
TestPlugin.Initialize(); ❶
}
// 使用它进行初始化
无效开始(){
消息 = "开始: " + TestPlugin.TestString("这是一个测试");
}
// 每帧调用一次更新
无效更新(){
// 确保用户触摸了屏幕
如果 (Input.touchCount==0){返回;}
触摸 touch = Input.GetTouch(0); ❷
如果(触摸.相位==TouchPhase.Began){
消息 = "触摸:" + TestPlugin.TestNumber();
}
}
无效的OnGUI(){
GUI.Label(new Rect(10,10,200,20),消息); ❸
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MobileTestObject : MonoBehaviour {
private string message;
void Awake() {
TestPlugin.Initialize(); ❶
}
// Use this for initialization
void Start() {
message = "START: " + TestPlugin.TestString("ThIs Is A tEsT");
}
// Update is called once per frame
void Update() {
// Make sure the user touched the screen
if (Input.touchCount==0){return;}
Touch touch = Input.GetTouch(0); ❷
if (touch.phase == TouchPhase.Began) {
message = "TOUCH: " + TestPlugin.TestNumber();
}
}
void OnGUI() {
GUI.Label(new Rect(10, 10, 200, 20), message); ❸
}
}
❶ Initialize the plugin at the beginning.
❸ Display a message in the corner of the screen.
此清单中的脚本初始化插件对象,然后调用插件方法来响应触摸输入。一旦此脚本在设备上运行,您将看到每当您点击屏幕时角落中的测试消息都会发生变化。
The script in this listing initializes the plugin object and then calls plugin methods in response to touch input. Once this is running on the device, you’ll see the test message in the corner change whenever you tap the screen.
最后要做的就是编写TestPlugin引用的本机代码。iOS 设备上的代码是使用 Objective C 和/或 C(或 Swift,但我们不会使用该语言)编写的,因此您需要一个 .h 头文件和一个 .mm 实现文件。如前所述,它们需要放在 Project 视图中的 Plugins/iOS/ 文件夹中。在那里创建TestPlugin.h和TestPlugin.mm;在 .h 文件中,编写此代码。
The final thing left to do is to write the native code that TestPlugin references. Code on iOS devices is written using Objective C and/or C (or Swift, but we won’t be using that language), so you need both a .h header file and a .mm implementation file. As described earlier, they need to go in the Plugins/iOS/ folder in the Project view. Create TestPlugin.h and TestPlugin.mm there; in the .h file, write this code.
清单 13.7 iOS 代码的 TestPlugin.h 头文件
Listing 13.7 TestPlugin.h header for iOS code
#导入 <Foundation/Foundation.h>
@接口测试对象:NSObject{
NSString* 状态;
}
@结尾#import <Foundation/Foundation.h>
@interface TestObject : NSObject {
NSString* status;
}
@end
查找有关 iOS 编程的解释,以了解此标头的作用;解释 iOS 编程超出了本书的范围。将此清单中的代码写入 .mm 文件中。
Look for an explanation about iOS programming to understand what this header is doing; explaining iOS programming is beyond the scope of this book. Write the code from this listing in the .mm file.
Listing 13.8 TestPlugin.mm implementation
#导入“测试插件.h”
@implementation 测试对象
@结尾
NSString* CreateNSString (const char* 字符串)
{
如果 (字符串)
返回 [NSString stringWithUTF8String: string];
别的
返回 [NSString stringWithUTF8String: ""];
}
char* MakeStringCopy(const char* 字符串)
{
如果 (字符串 == NULL)
返回 NULL;
char* res = (char*)malloc(strlen(字符串)+ 1);
strcpy(res,字符串);
返回 res;
}
外部“C”{
const char* _TestString(const char* 字符串) {
NSString * 旧字符串 = 创建NSString (字符串);
NSString* newString = [oldString lowercaseString];
返回 MakeStringCopy([newString UTF8String]);
}
浮点数_TestNumber() {
返回 (arc4random() % 100)/100.0f;
}
}#import "TestPlugin.h"
@implementation TestObject
@end
NSString* CreateNSString (const char* string)
{
if (string)
return [NSString stringWithUTF8String: string];
else
return [NSString stringWithUTF8String: ""];
}
char* MakeStringCopy (const char* string)
{
if (string == NULL)
return NULL;
char* res = (char*)malloc(strlen(string) + 1);
strcpy(res, string);
return res;
}
extern "C" {
const char* _TestString(const char* string) {
NSString* oldString = CreateNSString(string);
NSString* newString = [oldString lowercaseString];
return MakeStringCopy([newString UTF8String]);
}
float _TestNumber() {
return (arc4random() % 100)/100.0f;
}
}
再次强调,此代码的详细解释超出了本书的范围。请注意,许多字符串函数用于将 Unity 的字符串数据表示形式转换为本机代码。
Again, a detailed explanation of this code is a bit beyond this book’s scope. Note that many of the string functions are there to convert Unity’s representation of string data into the native code.
提示此示例仅单向通信,即从 Unity 到插件。但本机代码也可以使用UnitySendMessage()方法与 Unity 通信。你可以向场景中一个命名的对象发送消息;在初始化期间,插件创建了TestPlugin_instance来发送消息。
TIP This sample communicates in only one direction, from Unity to the plugin. But the native code could also communicate to Unity by using the UnitySendMessage() method. You can send a message to a named object in the scene; during initialization, the plugin created TestPlugin_instance to send messages to.
有了原生代码,您就可以构建 iOS 应用并在设备上进行测试。角落里的消息最初将全部为小写;然后点击屏幕即可查看显示的数字。非常酷!
With the native code in place, you can build the iOS app and test it on a device. The message in the corner will initially be all lowercase; then tap the screen to watch the numbers displayed. Very cool!
有关更多信息,请访问https://docs.unity3d.com/Manual/PluginsForIOS.html。以上就是如何制作 iOS 插件,接下来让我们看看 Android也。
For more information, visit https://docs.unity3d.com/Manual/PluginsForIOS.html. That’s how to make an iOS plugin, so let’s look at Android too.
到创建 Android 插件,Unity 方面几乎完全相同。您根本不需要更改MobileTestObject 。在TestPlugin中进行此处显示的添加。
To create an Android plugin, the Unity side of things is almost exactly the same. You don’t need to change MobileTestObject at all. Make the additions shown here in TestPlugin.
清单 13.9 修改TestPlugin以使用 Android 插件
Listing 13.9 Modifying TestPlugin to use the Android plugin
... #region iOS [DllImport(“__Internal”)] 私有静态外部浮点数_TestNumber(); [DllImport(“__Internal”)] 私有静态外部字符串_TestString(字符串测试); #endregion iOS #如果 UNITY_ANDROID 私有静态异常_pluginError; 私有静态 AndroidJavaClass _pluginClass; ❶ 私有静态 AndroidJavaClass GetPluginClass() { ❶ 如果 (_pluginClass == null && _pluginError == null) { ❶ AndroidJNI.附加当前线程(); 尝试 { _pluginClass = new AndroidJavaClass("com.testcompany.testplugin.TestPlugin"); ❷ } 捕获 (异常 e) { _pluginError = e; } } 返回 _pluginClass; } 私有静态AndroidJavaObject _unityActivity; 私有静态 AndroidJavaObject GetUnityActivity() { 如果 (_unityActivity == 空) { AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); ❸ _unityActivity = unityPlayer.GetStatic<AndroidJavaObject>("当前活动"); } 返回_unityActivity; } #结束 公共静态浮点测试编号() { 浮点数val = 0f; 如果(应用程序.平台==运行时平台.IPhonePlayer) val = _TestNumber(); #如果 UNITY_ANDROID 如果 (!Application.isEditor && _pluginError == null) val = GetPluginClass().CallStatic<int>("getNumber"); ❹ #结束 返回值; } 公共静态字符串 TestString(字符串测试){ 字符串val =“”; 如果(应用程序.平台==运行时平台.IPhonePlayer) val = _TestString(测试); #如果 UNITY_ANDROID 如果 (!Application.isEditor && _pluginError == null) val = GetPluginClass().CallStatic<string>("getString", 测试); #结束 返回值; } }
...
#region iOS
[DllImport("__Internal")]
private static extern float _TestNumber();
[DllImport("__Internal")]
private static extern string _TestString(string test);
#endregion iOS
#if UNITY_ANDROID
private static Exception _pluginError;
private static AndroidJavaClass _pluginClass; ❶
private static AndroidJavaClass GetPluginClass() { ❶
if (_pluginClass == null && _pluginError == null) { ❶
AndroidJNI.AttachCurrentThread();
try {
_pluginClass = new AndroidJavaClass("com.testcompany.testplugin.TestPlugin"); ❷
} catch (Exception e) {
_pluginError = e;
}
}
return _pluginClass;
}
private static AndroidJavaObject _unityActivity;
private static AndroidJavaObject GetUnityActivity() {
if (_unityActivity == null) {
AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); ❸
_unityActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
}
return _unityActivity;
}
#endif
public static float TestNumber() {
float val = 0f;
if (Application.platform == RuntimePlatform.IPhonePlayer)
val = _TestNumber();
#if UNITY_ANDROID
if (!Application.isEditor && _pluginError == null)
val = GetPluginClass().CallStatic<int>("getNumber"); ❹
#endif
return val;
}
public static string TestString(string test) {
string val = "";
if (Application.platform == RuntimePlatform.IPhonePlayer)
val = _TestString(test);
#if UNITY_ANDROID
if (!Application.isEditor && _pluginError == null)
val = GetPluginClass().CallStatic<string>("getString", test);
#endif
return val;
}
}
❶ AndroidJNI functionality provided by Unity
❷ Name of the class you programmed; change this name as needed.
❸ Unity creates an activity for the Android app.
❹ Call to functions in plugin .jar
你会注意到大多数添加都发生在UNITY_ANDROID中平台内定义。如本章前面所述,这些编译器指令使代码仅适用于某些平台,而在其他平台上则被省略。而 iOS 代码没有做任何会破坏其他平台的事情(它不会做任何事情,但也不会导致错误),Android 插件的代码只有在 Unity 设置为 Android 平台时才会编译。
You’ll notice that most of the additions happen inside UNITY_ANDROID platform defines. As explained earlier in the chapter, these compiler directives cause code to apply only to certain platforms and are omitted on other platforms. Whereas the iOS code wasn’t doing anything that would break on other platforms (it won’t do anything, but it won’t cause errors, either), the code for Android plugins will compile only when Unity is set to the Android platform.
特别要注意对AndroidJNI 的调用。这是 Unity 中用于连接原生 Android 的系统。出现的另一个可能令人困惑的词是Activity;在 Android 应用中,活动是应用进程。Unity 游戏是 Android 应用的活动,因此插件代码需要访问该活动以在需要时将其传递出去。
In particular, note the calls to AndroidJNI. That’s the system within Unity for connecting to native Android. The other possibly confusing word that appears is Activity; in Android apps, an activity is an app process. The Unity game is an activity of the Android app, so the plugin code needs access to that activity to pass it around when needed.
最后,您需要原生 Android 代码。iOS 代码是用 Objective C 和 C 等语言编写的,而 Android 是用 Java(或 Kotlin,但我们将使用 Java)编程的。但您不能简单地为插件提供原始 Java 代码;插件必须是从 Java 代码打包的 JAR。在这里,再次强调,Android 编程的细节超出了 Unity 介绍的范围,但我们将简要介绍一下基础知识。首先,如果您在下载 Android SDK 时没有安装 Android Studio,则应该安装它。
Finally, you need the native Android code. Whereas iOS code is written in languages like Objective C and C, Android is programmed in Java (or Kotlin, but we’ll use Java). But you can’t simply provide the raw Java code for the plugin; the plugin must be a JAR packaged from the Java code. Here, again, the details of Android programming are out of scope for an introduction to Unity, but we’ll go over the basics briefly. First off, you should install Android Studio if you didn’t do so as part of downloading the Android SDK.
图 13.7 说明了在 Android Studio 中设置插件项目的步骤(来自 4.2.1 版本的屏幕截图):
Figure 13.7 illustrates the steps to set up a plugin project in Android Studio (with screenshots from version 4.2.1):
Create a New Project by either selecting that in the startup window or going to File > New > New Project.
In the New Project window that appears, select the No Activity template (since this is a plugin, not a standalone Android app) and click Next.
现在将其命名为TestPluginProj;对于此测试,Min SDK 是什么并不重要,但将语言保留为 Java 并记下项目位置,因为您稍后需要找到它。单击“完成”以创建新项目,如果需要等待加载,请再次单击“完成”以关闭窗口。
Now name it TestPluginProj; for this test, it doesn’t matter what the Min SDK is, but leave Language as Java and take note of the project location because you’ll need to find it later. Click Finish to create the new project, and if there is a brief wait for loading, then click Finish again to dismiss the window.
Once the editor view appears, choose File > New > New Module to add a library.
选择 Android Library,将其命名为testplugin,将 Package Name 更改为com.testcompany.testplugin,然后单击 Finish。
Select Android Library, name it testplugin, change Package Name to com.testcompany.testplugin, and then click Finish.
添加该模块后,选择 Build > Select Build Variant;在打开的面板中,单击 TestPluginProj.testplugin 的 Active Build Variant 并选择 Release。
With that module added, choose Build > Select Build Variant; in the panel that opens, click the Active Build Variant for TestPluginProj.testplugin and select Release.
现在在上面的项目面板中展开 testplugin > java,右键单击 com.testcompany.testplugin,并选择新建 > Java 类。
Now expand testplugin > java in the upper Project panel, right-click com.testcompany.testplugin, and choose New > Java Class.
A tiny window opens to configure the new class, so type the name TestPlugin and press Enter.
图 13.7 设置 Android Studio 来构建插件
Figure 13.7 Setting up Android Studio to build a plugin
TestPlugin目前为空,因此请在其中编写插件函数。清单 13.10 显示了该插件的 Java 代码。
TestPlugin is currently empty, so write the plugin functions in it. Listing 13.10 shows the Java code for the plugin.
清单 13.10 编译成 JAR 的 TestPlugin.java
Listing 13.10 TestPlugin.java that compiles into a JAR
包 com.testcompany.testplugin;
公共类测试插件{
私有静态整数 = 0;
公共静态int getNumber(){
数字++;
返回号码;
}
公共静态字符串 getString (字符串消息){
返回消息.toLowerCase();
}
}package com.testcompany.testplugin;
public class TestPlugin {
private static int number = 0;
public static int getNumber() {
number++;
return number;
}
public static String getString(String message) {
return message.toLowerCase();
}
}
好了,现在您可以将此代码打包成 JAR(或者更确切地说是包含 JAR 的 Android Archive 文件)。在顶部菜单中,选择 Build > Make Project。构建完成后,转到计算机上的项目并在 < project location >/testplugin/build/outputs/aar/ 中找到 testplugin-release.aar。将存档文件拖到 Unity 的 Android plugins 文件夹中以将其导入。
All right, now you can package this code into a JAR (or rather an Android Archive file, which contains the JAR). In the top menu, choose Build > Make Project. Once the build is complete, go to the project on your computer and find testplugin-release.aar in <project location>/testplugin/build/outputs/aar/. Drag the archive file into Unity’s Android plugins folder to import it.
使用 Plugins/Android 中的存档文件,构建游戏并将其安装在设备上,每当您点击屏幕时,消息就会发生变化。此外,与 iOS 插件一样,Android 插件可以使用UnityPlayer.UnitySendMessage()与场景中的对象进行通信。Java 代码需要导入 Unity 的 Android Player 库,该库包含在 Unity 安装文件夹中(同样,通常是 Windows 上的 C:\Program Files\ Unity\Editor\Data 或 Mac 上的 /Applications/Unity/Editor),文件名称为 /PlaybackEngines/AndroidPlayer/Variations/mono/Release/Classes/classes.jar。
With the archive file in Plugins/Android, build the game and install it on a device, and the message will change whenever you tap the screen. Also, like the iOS plugin, an Android plugin could use UnityPlayer.UnitySendMessage()to communicate with the object in the scene. The Java code would need to import Unity’s Android Player library, which is contained in the Unity installation folder (again, usually C:\Program Files\ Unity\Editor\Data on Windows or /Applications/Unity/Editor on Mac) as /PlaybackEngines/AndroidPlayer/Variations/mono/Release/Classes/classes.jar.
我知道我忽略了很多关于开发 Android 库的内容,但那是因为这个过程既复杂又经常变化。如果你足够高级,可以为你的 Android 游戏开发插件,你就必须查阅 Android 开发者网站上的文档,并参考 Unity 的文档在http://mng.bz/yJKG。
I know I glossed over a lot in developing Android libraries, but that’s because the process is both complicated and changes frequently. If you become advanced enough to develop plugins for your Android games, you’re going to have to look up documentation on Android’s developer website, as well as refer to Unity’s documentation at http://mng.bz/yJKG.
注:XR 的首字母缩写代表扩展现实,该术语涵盖虚拟现实(VR) 和增强现实(AR)。VR 是指让用户沉浸在完全合成的环境中,而 AR 是指将计算机图形添加到自然环境中,但两者都属于调节用户周围环境的技术范畴。
NOTE The initials XR stand for extended reality, a term that encompasses both virtual reality (VR) and augmented reality (AR). VR refers to immersing the user in a completely synthetic environment, while AR refers to adding computer graphics to the natural environment, but both fall under the umbrella of technologies that mediate the environment surrounding the user.
扩展是本章中介绍的最后一个“平台”。之所以用引号表示“平台”,是因为从技术上讲,在构建应用程序时,XR 不被视为单独的平台。相反,XR 支持来自可以添加到相关构建平台(例如桌面 VR 或移动 AR)的插件包。让我们先了解一下它的工作原理,首先是 VR,然后是 AR。
XR is the last “platform” covered in this chapter. “Platform” is in quotes because XR isn’t technically considered a separate platform when building the application. Instead, XR support comes from plugin packages that can be added to the relevant build platforms, such as desktop VR or mobile AR. Let’s go over how this works, first for VR and then AR.
这目前市场上的主要 VR 设备是 Oculus Quest、HTC VIVE、Valve Index 和 PlayStation VR。除了 PlayStation VR(因为本书不涉及主机开发),所有其他设备都可以通过向 Unity 的 PC 构建目标或(对于 Oculus Quest)向 Android 添加 VR SDK 来支持。
The major VR devices on the market right now are Oculus Quest, HTC VIVE, Valve Index, and PlayStation VR. Ignoring PlayStation VR (since this book doesn’t cover console development), all the other devices are supported by adding a VR SDK to either Unity’s PC build target, or (in the case of Oculus Quest) to Android.
有多种这样的 SDK 可用,它们通过 Unity 的 Package Manager 分发。例如,浏览 Unity Registry 以查找 Oculus XR 或 Windows XR 等选项。同时,为 Unity 开发人员提供的另一个有吸引力的选项是 XR Interaction Toolkit,但该软件包稍微难找一些。由于该软件包仍不完整(不过,AR 支持大多不完整;VR 支持相当可靠),因此它被视为预览包。默认情况下不显示指定为预览的软件包,但您可以调整 Package Manager 窗口的设置以显示预览包(见图 13.8)。
A variety of such SDKs are available, distributed through Unity’s Package Manager. For example, browse the Unity Registry to find options like Oculus XR or Windows XR. Meanwhile, another attractive option offered to Unity developers is XR Interaction Toolkit, but that package is slightly harder to find. Because that package is still not considered complete (mostly incomplete in AR support, though; the VR support is pretty solid), it is considered a preview package. Packages designated as preview aren’t shown by default, but you can adjust the settings of the Package Manager window to show preview packages (see figure 13.8).
Figure 13.8 How to see preview packages in Package Manager
一旦安装了 XR 包,您必须在 XR 插件管理下的项目设置(记住,是编辑 > 项目设置)中启用它(如图 13.9 所示)。
Once an XR package is installed, you must enable it in Project Settings (remember, that’s Edit > Project Settings) under XR Plug-in Management (shown in figure 13.9).
Figure 13.9 XR Plugin Management in Project Settings
注意: XR 插件管理本身是一个包,尽管它应该与您选择的任何其他 XR 包一起安装。但如果这些设置未出现,您可能需要手动安装该包。
NOTE XR Plug-in Management is itself a package, although that should have been installed along with whatever other XR package you chose. If those settings aren’t appearing, though, you may need to install the package manually.
我们不会介绍任何特定 VR 设备的代码,因为要介绍的选项实在太多了。相反,我鼓励您访问相关 XR 插件的文档:
We’re not going to go over code for any specific VR device, because there are just too many options to cover. Instead, I encourage you visit the documentation for the relevant XR plugin:
XR交互工具包:http://mng.bz/Mv67
XR Interaction Toolkit: http://mng.bz/Mv67
Oculus XR:http://mng.bz/aZjz
Oculus XR: http://mng.bz/aZjz
Windows XR:http://mng.bz/g16l
Windows XR: http://mng.bz/g16l
OpenXR: http: //mng.bz/ePNz
OpenXR: http://mng.bz/ePNz
We are, however, going to implement a simple example to help explain AR.
不同于VR、增强现实并不一定意味着需要头戴式显示器(HMD)。当然,它可以涉及 HMD,Unity 支持 HoloLens 和 Magic Leap 等设备。但是,AR 也可以通过手机提供,有时也称为手持 AR。
Unlike VR, augmented reality doesn’t necessarily imply a head-mounted display (HMD). It certainly can involve an HMD, and Unity supports devices like the HoloLens and Magic Leap. However, AR also is provided through mobile phones, what’s sometimes referred to as handheld AR.
Apple 和 Google 分别在 iOS 和 Android 上提供手持 AR 的 SDK。Apple 的 SDK 称为 ARKit,而 Google 提供的是 ARCore。然而,这些库是特定于这些平台的,因此 Unity 提供了一个名为AR Foundation 的跨平台包装器。作为开发人员,重要的是要了解您在后台使用 ARKit 或 ARCore,但您针对 AR Foundation 的 API 进行编码。
Both Apple and Google provide SDKs for handheld AR on iOS and Android, respectively. Apple’s SDK is called ARKit, while Google provides ARCore. These libraries are specific to those platforms however, so Unity provides a cross-platform wrapper called AR Foundation. As a developer, it’s important to understand that you are working with ARKit or ARCore under the hood, but you code against the API of AR Foundation.
首先,创建一个新的 Unity 项目。在这个新项目中,转到 Package Manager 并安装 AR Foundation,以及 ARKit XR 或 ARCore XR(或两者!),具体取决于您为哪个移动平台开发。然后在 XR 插件管理中启用 ARKit 或 ARCore(如图 13.9 所示)。
To start with, create a new Unity project. In this new project, go to Package Manager and install AR Foundation, along with either ARKit XR or ARCore XR (or both!), depending on which mobile platform you are developing for. Then enable ARKit or ARCore in XR Plug-in Management (shown back in figure 13.9).
注意: ARKit 的面部追踪部分与 ARKit 的其余部分有一个单独的软件包。这是因为 Apple 会拒绝提交包含面部追踪代码但实际上不进行面部 AR 的应用程序。因此,如果您不进行面部 AR,请仅安装主 ARKit XR 插件包;如果您进行面部 AR,请同时安装这两个软件包。
NOTE The face-tracking bit of ARKit has a separate package from the rest of ARKit. That’s because Apple will reject submitted apps that have code for face-tracking but aren’t actually doing facial AR. Thus, install only the main ARKit XR plug-in package if you aren’t doing facial AR, and install both packages if you are.
ARKit 和 ARCore 在 iOS 和 Android 平台的播放器设置中必须满足一些要求(见图 13.10a 和 b)。在 Android 上,首先从图形 API 列表中删除 Vulkan(选择 Vulkan,然后单击减号按钮),然后向下滚动并将最低 API 级别更改为 24。在 iOS 上,将最低 iOS 版本设置为 11,确保架构设置为 ARM64,打开需要 ARKit 设置,然后输入相机使用说明(例如AR所需的相机 )。
ARKit and ARCore have requirements that must be met in the Player settings for the iOS and Android platforms (see figure 13.10a and b). On Android, first remove Vulkan from the list of Graphics APIs (select Vulkan and then click the minus button), then scroll down and change the Minimum API Level to 24. On iOS, set the Minimum iOS Version to 11, make sure Architecture is set to ARM64, turn on the Requires ARKit setting, and enter a camera usage description (something like Camera required for AR).
Figure 13.10a Adjust Android settings to support AR
Figure 13.10b Adjust iOS settings to support AR
ARKit 需要这些 iOS 设置才能运行,而 ARCore 需要这些 Android 设置。在播放器设置中进行所有必要的调整后,接下来设置场景中所需的各种对象。如图 13.11 所示,要采取的步骤如下:
ARKit requires those iOS settings to function, and ARCore requires those Android settings. Having made all the necessary adjustments in Player settings, next set up the various objects needed in the scene. As depicted in figure 13.11, the steps to take are as follows:
接下来,创建一个名为PlaneTrackingController的新 C# 脚本,并将清单 13.11 写入其中。
Next, create a new C# script called PlaneTrackingController, and write listing 13.11 into it.
清单 13.11使用 AR Foundation 的PlaneTrackingController脚本
Listing 13.11 PlaneTrackingController script that uses AR Foundation
使用System.Collections;
使用 System.Collections.Generic;
使用 UnityEngine;
使用 UnityEngine.XR.ARFoundation;
使用 UnityEngine.XR.ARSubsystems;
公共类 PlaneTrackingController:MonoBehaviour {
[SerializeField] ARSessionOrigin arOrigin = null;
[SerializeField] GameObject planePrefab = null; ❶
私人ARPlaneManager平面管理器;
无效开始(){
planeManager = arOrigin.gameObject.AddComponent<ARPlaneManager>(); ❷
平面管理器.检测模式 = 平面检测模式.水平;
planeManager.planePrefab =planePrefab;
}
}using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
public class PlaneTrackingController : MonoBehaviour {
[SerializeField] ARSessionOrigin arOrigin = null;
[SerializeField] GameObject planePrefab = null; ❶
private ARPlaneManager planeManager;
void Start() {
planeManager = arOrigin.gameObject.AddComponent<ARPlaneManager>(); ❷
planeManager.detectionMode = PlaneDetectionMode.Horizontal;
planeManager.planePrefab = planePrefab;
}
}
❶这应该是来自 XR 对象的飞机预制件,而不仅仅是任何游戏对象。
❶ This should be the plane prefab from XR objects, not just any game object.
❷ It would also work to add this component in the editor, but we’ll add it in code.
此脚本将一个名为ARPlaneManager 的组件添加到会话原点,然后为平面管理器分配一些设置,包括使用哪个对象来可视化检测到的平面。此组件可以在编辑器中添加,但在代码中添加它可以为控制 AR 提供更大的灵活性。
This script adds a component called ARPlaneManager to the session origin and then assigns a couple of settings to the plane manager, including which object to use for visualizing the detected plane. This component could have been added in the editor, but adding it in code affords more flexibility in controlling the AR.
将此脚本拖到 Controllers 对象上,将其链接为组件。现在(如图 13.11 所示),将 AR Session Origin 和 AR Default Plane 拖到 Inspector 中的组件槽上。
Drag this script onto the Controllers object to link it as a component. Now (as figure 13.11 shows), drag AR Session Origin and AR Default Plane onto their component slots in the Inspector.
Figure 13.11 Setting up objects in the scene for simple AR
一切准备就绪后,构建移动应用以查看平面跟踪功能。由于PlaneTrackingController使用 AR Foundation(而不是直接使用 ARKit 或 ARCore),因此该项目应适用于 iOS 和 Android。一旦应用在您的设备上运行,当您移动相机时,您应该会看到类似图 13.12 的内容。
With everything in place, build the mobile app in order to see plane tracking functioning. Because PlaneTrackingController uses AR Foundation (rather than either ARKit or ARCore directly), the project should work on both iOS and Android. Once the app is running on your device, you should see something like figure 13.12 when moving the camera around.
Figure 13.12 AR plane detection in action
太好了,环境中检测到了平面!但是,现在除了计算机检测到表面之外什么都没有发生。也就是说,没有任何东西被放置在检测到的表面上。AR Foundation 提供了几个有用的功能,不仅仅是平面跟踪,另一个有用的功能是对检测到的 AR 表面进行光线投射。按照清单 13.12 添加用于执行 AR 光线投射的代码。
Great, planar surfaces are being detected in the environment! However, right now nothing is going on other than that the computer detects surfaces. That is, nothing is being placed on the detected surface. AR Foundation provides several useful bits of functionality, not just plane tracking, and another useful feature is raycasting against detected AR surfaces. Follow listing 13.12 to add code for doing AR raycasting.
清单 13.12 将光线投射添加到PlaneTrackingController
Listing 13.12 Adding raycasting to PlaneTrackingController
...
私人ARPlaneManager平面管理器;
私有ARRaycastManager raycastManager; ❶
私人游戏对象原;
...
无效开始(){
prim = GameObject.CreatePrimitive(PrimitiveType.Cube); ❷
prim.设置活动(false);
raycastManager = arOrigin.gameObject.AddComponent<ARRaycastManager>();
planeManager = arOrigin.gameObject.AddComponent<ARPlaneManager>();
...
}
无效更新(){
如果(输入.GetMouseButtonDown(0)){
var hits = new List<ARRaycastHit>();
如果(raycastManager.Raycast(Input.mousePosition,命中, ❸
TrackableType.PlaneWithinPolygon)) {
prim.设置活动(true);
prim.transform.localScale = new Vector3(.1f, .1f, .1f);
var pose = hits[0].pose;
prim.transform.localPosition = 姿势.位置;
prim.transform.localRotation = 姿势.旋转;
}
}
}
......
private ARPlaneManager planeManager;
private ARRaycastManager raycastManager; ❶
private GameObject prim;
...
void Start() {
prim = GameObject.CreatePrimitive(PrimitiveType.Cube); ❷
prim.SetActive(false);
raycastManager = arOrigin.gameObject.AddComponent<ARRaycastManager>();
planeManager = arOrigin.gameObject.AddComponent<ARPlaneManager>();
...
}
void Update() {
if (Input.GetMouseButtonDown(0)) {
var hits = new List<ARRaycastHit>();
if (raycastManager.Raycast(Input.mousePosition, hits, ❸
TrackableType.PlaneWithinPolygon)) {
prim.SetActive(true);
prim.transform.localScale = new Vector3(.1f, .1f, .1f);
var pose = hits[0].pose;
prim.transform.localPosition = pose.position;
prim.transform.localRotation = pose.rotation;
}
}
}
...
❶ Add the new fields just under the existing manager.
❷ Create an object to place on detected surfaces.
❸ Call the Raycast method in response to user input.
再次将应用部署到您的移动设备上。这一次,点击检测到的平面,应该会出现一个立方体,如图 13.13 所示。这样,您就可以在真实环境周围放置虚拟物体。
Deploy the app again onto your mobile device. This time, tap the detected plane, and a cube should appear, just like figure 13.13. In this way, you are placing virtual objects around your real environment.
Figure 13.13 A cube placed on the tracked plane
此示例仅涉及 AR Foundation 的最基本内容。有关更深入的使用,请参阅 Unity 手册 ( http://mng.bz/p9aG ) 以及 Unity 在 GitHub 上的示例项目 ( http://mng.bz/YwpN )。
This example touches on only the very basics of AR Foundation. For more in-depth uses, refer to Unity’s manual (http://mng.bz/p9aG) as well as the sample projects Unity has on GitHub (http://mng.bz/YwpN).
Congratulations, you’ve reached the end!
恭喜,您现在了解了在大多数主要平台上部署 Unity 游戏的步骤。所有平台的基本构建过程都很简单(只需一个按钮),但在各种平台上自定义应用程序可能会变得复杂。现在,您已准备好开始构建您的自己的游戏!
Congratulations, you now know the steps for deploying a Unity game on most major platforms. The basic build process for all platforms is simple (just a single button), but customizing the app on various platforms can get complicated. Now you’re ready to get out there and build your own games!
Unity can build executable applications for a huge variety of platforms, including desktop computers, mobile devices, and websites.
A host of settings can be applied to builds, including details like the icon for the app and the name that appears.
Web games can interact with the web page they’re embedded in, allowing for all kinds of interesting web apps.
Unity supports custom plugins in order to extend its functionality.
到目前为止,您已经了解了使用 Unity 构建完整游戏所需的一切知识——从编程角度而言,所有知识都已掌握;一流的游戏还需要出色的艺术和音效。但要想成为一名成功的游戏开发者,需要的不仅仅是技术技能。让我们面对现实吧——学习 Unity 并不是您的最终目标。您的目标是创建成功的游戏,而 Unity 只是帮助您实现这一目标的工具(当然,是一款非常好的工具)。
At this point, you know everything you need to know to build a complete game using Unity—everything from a programming standpoint, that is; a top-notch game needs fantastic art and sound too. But success as a game developer involves a lot more than technical skills. Let’s face it—learning Unity isn’t your end goal. Your goal is to create successful games, and Unity is just a tool (granted, a very good tool) to get you to that goal.
除了实现游戏中所有功能的技术技能外,您还需要一个额外的无形属性:毅力。我说的是坚持不懈和自信,能够继续完成具有挑战性的项目并坚持到底——我有时称之为“完成能力”。只有一种方法可以提高您的完成能力,那就是完成大量项目。这似乎是一个两难的境地(要获得完成项目的能力,您首先需要完成大量项目),但要认识到的关键点是,小项目比大项目更容易完成。
Beyond the technical skills to implement everything in the game, you need an additional intangible attribute: grit. I’m talking about the doggedness and confidence to keep working on a challenging project and see it through to the end—what I sometimes refer to as “finishing ability.” There’s only one way to build up your finishing ability, and that’s to complete lots of projects. That seems like a catch-22 (to gain the ability to complete projects, you first need to complete a lot of projects), but the key point to recognize is that small projects are way easier to complete than large ones.
因此,前进的道路是先建立许多小项目(因为这些项目很容易完成),然后再逐步实现更大的项目。许多新游戏开发者犯了一个错误,即处理一个太大的项目,主要有两个原因:他们想复制自己最喜欢的(大型)游戏,而且每个人都低估了制作一款游戏需要多少工作量。项目似乎开始得很好,但很快就陷入了太多挑战的泥潭,最终开发者感到沮丧并退出。
Therefore, the path forward is to first build a lot of small projects—because those are easy to complete—and work up to larger projects. Many new game developers make the mistake of tackling a project that’s too large for two main reasons: they want to copy their favorite (big) game, and everyone underestimates how much work it takes to make a game. The project seemingly starts off fine but quickly gets bogged down in too many challenges, and eventually the developer gets dejected and quits.
相反,游戏开发新手应该从小处着手。从小到微不足道的项目开始。本书中的项目就是您应该从中开始的那种“小到几乎微不足道”的项目。如果您已经完成了本书中的所有项目,那么您已经完成了许多这样的入门项目。在下一个项目中尝试更大的东西,但要小心不要跨得太大。您将积累技能和信心,因此每次都可以更有野心。
Instead, someone new to game development should start small. Start with projects so small that they seem trivial. The projects in this book are the sort of “small, almost to the point of trivial” projects that you should start with. If you’ve done all the projects in this book, you’ve already gotten a lot of these starter projects out of the way. Try something bigger for your next project, but be wary of making too big a jump. You’ll build up your skills and confidence, so you can get a little more ambitious each time.
几乎每次您问如何开始开发游戏时,您都会听到同样的建议。例如,Unity 要求网络系列Extra Credits(关于游戏开发的精彩系列)制作一些关于游戏开发入门的视频,您可以在http://mng.bz/GOjq找到它们。
You’ll hear this same advice almost anytime you ask how to start developing games. For example, Unity asked the web series Extra Credits (a great series about game development) to make some videos about starting in game dev, and you’ll find them at http://mng.bz/GOjq.
整个Extra Credits系列远远超出了 Unity 赞助的这几个视频。它涵盖了很多内容,但主要侧重于游戏设计原则。
The entire Extra Credits series goes way beyond this handful of videos sponsored by Unity. It covers a lot of ground but mostly focuses on the discipline of game design.
定义 游戏设计是通过创建游戏目标、规则和挑战来定义游戏的过程。它不要与视觉设计相混淆,视觉 设计是设计外观,而不是功能。这是一个常见的错误,因为普通人最熟悉“平面设计”中的“设计”。
DEFINITION Game design is the process of defining a game by creating its goals, rules, and challenges. It is not to be confused with visual design, which is designing appearance, not function. This is a common mistake because the average person is most familiar with “design” in the context of “graphic design.”
定义游戏设计最核心的部分之一是设计游戏机制——游戏中的单个动作(或动作系统)。游戏中的机制通常由游戏规则设定,而游戏中的挑战通常来自于将机制应用于特定情况。例如,在游戏中走动是一种机制;迷宫是一种基于该机制的挑战。
DEFINITION One of the most central parts of game design is crafting game mechanics—the individual actions (or systems of actions) within a game. The mechanics in a game are often set up by its rules, whereas the challenges in a game generally come from applying the mechanics to specific situations. For example, walking around the game is a mechanic; a maze is a kind of challenge based on that mechanic.
对于游戏开发新手来说,思考游戏设计可能很棘手。最成功(并且制作起来最令人满意!)的游戏都是采用有趣且创新的游戏机制构建的。相反,过于担心第一款游戏的设计可能会分散您对游戏开发其他方面的注意力,例如学习如何编程。您最好从模仿现有游戏的设计开始。(请记住,我只是在谈论刚开始;复制现有游戏对于初始练习很有用,但最终您将拥有足够的技能和经验来进一步发展。)
Thinking about game design can be tricky for newcomers to game development. The most successful (and satisfying to create!) games are built with interesting and innovative game mechanics. Conversely, worrying too much about the design of your first game can distract you from other aspects of game development, like learning how to program it. You’re better off starting out by aping the design of existing games. (Remember, I’m only talking about starting out; cloning existing games is great for initial practice, but eventually you’ll have enough skills and experience to branch out further.)
尽管如此,任何成功的游戏开发者都应该对游戏设计充满好奇。有很多方法可以了解更多有关游戏设计的知识 - 您已经了解了Extra Credits视频,但这里还有一些其他网站:
That said, any successful game developer should be curious about game design. There are lots of ways to learn more about game design—you already know about the Extra Credits videos, but here are some other websites:
www.gamedeveloper.com —提供工作、游戏更新、有关游戏的好消息/坏消息;您想要/需要了解的有关游戏制作艺术和商业的一切信息。
www.gamedeveloper.com—Offers jobs, games updates, good news/bad news about games; everything you want/need to know about the art and business of making games.
https://lostgarden.home.blog/worth-reading/ — 关于游戏设计理论、艺术和设计业务的可读且深思熟虑的文章。
https://lostgarden.home.blog/worth-reading/—Readable, thoughtful essays on game design theory, art, and the business of design.
http://sloperama.com —点击 School-a-rama 进入游戏业务建议页面。
http://sloperama.com—Click School-a-rama for the game biz advice page.
Great books on the subject are also available, such as the following:
The Art of Game Design, Third Edition, by Jesse Schell (A K Peters/CRC Press, 2019)
Game Design Workshop, Fourth Edition, by Tracy Fullerton (A K Peters/CRC Press, 2018)
A Theory of Fun for Game Design, Second Edition, by Raph Koster (O’Reilly Media, 2013)
在Extra Credits视频中,第四个视频是关于营销游戏的。有时游戏开发者会推迟考虑营销。他们只想着开发游戏,而不去营销,但这种态度可能会导致游戏失败。如果没有人知道,即使是世界上最好的游戏也不会成功!
In the Extra Credits videos, the fourth video is about marketing your game. Sometimes game developers put off thinking about marketing. They want to think only about building the game and not marketing it, but that attitude will probably result in a failed game. The best game in the world still won’t be successful if nobody knows about it!
营销这个词经常会让人联想到广告,如果你有预算,那么为你的游戏投放广告无疑是营销的一种方式。但你可以用很多低成本甚至免费的方式来宣传你的游戏。具体方法往往会随着时间的推移而改变,但该视频中提到的总体策略包括在推特上发布你的游戏(或在社交媒体上发布,而不仅仅是推特)以及制作预告片视频,在 YouTube 上与评论者、博主等分享。坚持不懈,发挥创造力!
The word marketing often evokes thoughts of ads, and if you have the budget, then running ads for your game is certainly one way to market it. But you can get the word out about your game in lots of low cost or even free ways. Specifics tend to change over time, but overall strategies mentioned in that video include tweeting about your game (or posting on social media in general, not just Twitter) and creating a trailer video to share on YouTube with reviewers, bloggers, and so on. Be persistent and get creative!
现在去创建一些很棒的游戏吧。Unity 是一款出色的工具,可以实现这一目标,而且您已经学会了如何使用它。祝您旅途愉快!
Now go and create some great games. Unity is an excellent tool for doing just that, and you’ve learned how to use it. Good luck on your journey!
Unity 的操作是通过鼠标和键盘进行的,但对于新手来说,鼠标和键盘在 Unity 中的使用方式并不明显。具体来说,最基本的鼠标和键盘输入是在场景中导航并查看 3D 对象。Unity 还提供了用于常用操作的键盘命令。
Operating Unity is done through the mouse and keyboard, but it isn’t obvious to a newcomer how the mouse and keyboard are used in Unity. In particular, the most basic sort of mouse and keyboard input is navigating around the scene and looking around the 3D objects. Unity also has keyboard commands for commonly used operations.
我将在这里解释输入控制,但您也可以参考几个网页(这些是 Unity 在线手册中的相关页面):
I’ll explain the input controls here, but you also can refer to a couple of web pages (these are the relevant pages in Unity’s online manual):
场景导航主要通过三种主要导航操作完成:移动、轨道和缩放。这三种操作涉及单击和拖动,同时按住 Alt(或 Mac 上的 Option)和 Ctrl(Mac 上的 Command)。一键、两键和三键鼠标的具体控制各不相同;表 A.1 列出了所有控制。
Scene navigation is primarily done with three main navigation maneuvers: Move, Orbit, and Zoom. The three movements involve clicking and dragging while holding down a combination of Alt (or Option on the Mac) and Ctrl (Command on a Mac). The exact controls vary for one-, two-, and three-button mice; table A.1 lists all the controls.
Table A.1 Scene navigation controls for various kinds of mice
注意尽管 Unity 可以使用一键或两键鼠标,但我强烈建议使用三键鼠标(是的,三键鼠标在 Mac 上运行良好)。
NOTE Although Unity can be used with one- or two-button mice, I highly recommend getting a three-button mouse (and yes, a three-button mouse works fine on a Mac).
除了使用鼠标进行的导航操作外,一些视图控制也基于键盘。如果按住鼠标右键,则可以使用键盘上的 W、A、S、D 键以大多数第一人称游戏中常见的方式四处走动。在执行任何其他控制时按住 Shift 可加快移动速度。
Besides the navigation maneuvers done using the mouse, some view controls are based on the keyboard. If you hold down the right button on the mouse, the W, A, S, D keys on the keyboard can be used to walk around in the manner common to most first-person games. Hold Shift during any other control to move faster.
但最重要的是,如果你在选择对象时按下 F 键,场景视图将平移和缩放以聚焦于该对象。如果你在浏览场景时迷路了,一个常见的“逃生舱”是选择层次结构中列出的对象,将鼠标移到场景视图上(此快捷方式仅在该视图中有效),然后按 F。
But most important, if you press F while an object is selected, the Scene view will pan and zoom to focus on that object. If you get lost while navigating your scene, a common “escape hatch” is to select an object listed in the Hierarchy, move the mouse over the Scene view (this shortcut works only while in that view), and then press F.
统一具有键盘命令,可快速访问重要功能。最重要的键盘快捷键是 W、E、R 和 T:这些键可激活变换工具平移、旋转和缩放(如果您不记得变换工具的作用,请参阅第 1 章)以及 2D 矩形工具。由于这些键彼此相邻,因此通常会将左手放在这些键上,而右手操作鼠标。
Unity has keyboard commands to quickly access important functions. The most important keyboard shortcuts are W, E, R, and T: those keys activate the transform tools Translate, Rotate, and Scale (refer to chapter 1 if you don’t recall what the transform tools do), as well as the 2D Rect tool. Because those keys are right next to each other, it’s common to leave your left hand on those keys while your right hand operates the mouse.
除了变换工具之外,您还可以使用键盘快捷键。表 A.2 列出了 Unity 中许多有用的键盘快捷键。
In addition to the transform tools, you can use keyboard shortcuts. Table A.2 lists many useful keyboard shortcuts in Unity.
Table A.2 Useful keyboard shortcuts
Unity 也能响应其他键盘快捷键,但列表越往后,它们的显示就越模糊我们得到了。
Unity responds to other keyboard shortcuts as well, but they get increasingly obscure the further down the list we get.
开发使用 Unity 开发的游戏依赖于各种外部软件工具来处理各种任务。在第 1 章中,我们讨论了一种外部工具:Visual Studio,虽然它与 Unity 捆绑在一起,但从技术上讲它是一个单独的应用程序。同样,开发人员也依赖一系列外部工具来完成 Unity 内部以外的工作。
Developing a game using Unity relies on a variety of external software tools for taking care of various tasks. In chapter 1, we discussed one external tool: Visual Studio, which is technically a separate application, even though it’s bundled along with Unity. In a similar manner, developers rely on an array of external tools to do work not internal to Unity.
这并不是说 Unity 缺乏它应有的功能。相反,游戏开发过程非常复杂且多面,任何设计良好、重点明确、关注点清晰的软件都不可避免地会局限于该过程的有限子集。在这种情况下,Unity 专注于成为将游戏的所有内容整合在一起并使其正常运行的粘合剂和引擎。创建所有这些内容都是使用其他工具完成的;让我们来看看几类可能对您有用的软件。
This isn’t to say that Unity is lacking capabilities that it ought to have. Rather, the game development process is so complex and multifaceted that any well-designed piece of software with a clear focus and clean separation of concerns will inevitably limit itself to being good at a limited subset of the process. In this case, Unity concentrates on being the glue and the engine that brings together all the content of a game and makes it function. Creating all that content is done with other tools; let’s take a look at several categories of software that could be useful to you.
我们我们已经了解了 Visual Studio,它是与 Unity 一起使用的最重要的编程工具。但您应该了解其他编程工具,正如您将在本节中看到的那样。
We’ve already looked at Visual Studio, the most significant programming tool used alongside Unity. But you should be aware of other programming tools, as you’ll see in this section.
作为如第 1 章所述,尽管 Unity 附带一种 Visual Studio 版本,但您可以选择使用其他 IDE。最常见的替代方案是 Visual Studio Code 或 JetBrains Rider。Rider ( www.jetbrains.com/lp/dotnet-unity/ ) 是一个功能强大的 C# 编程环境,带有 Unity一体化。
As mentioned in chapter 1, although Unity comes with one flavor of Visual Studio, you could choose to use a different IDE instead. The most common alternatives are either Visual Studio Code or JetBrains Rider. Rider (www.jetbrains.com/lp/dotnet-unity/) is a powerful C# programming environment with Unity integration.
Xcode是 Apple 提供的编程环境(特别是 IDE,但也包括适用于 Apple 平台的 SDK)。虽然您仍可以在 Unity 中完成绝大多数工作,但您需要使用 Xcode(https://developer.apple.com/xcode/)将游戏部署到 iOS。这项工作通常涉及使用Xcode。
Xcode is the programming environment provided by Apple (in particular, an IDE, but also including SDKs for Apple platforms). Although you’d still be doing the vast majority of the work within Unity, you need to use Xcode (https://developer.apple.com/xcode/) to deploy a game to iOS. That work often involves debugging or profiling your app by using the tools in Xcode.
只是由于您需要安装 Xcode 才能部署到 iOS,因此您需要下载 Android SDK 才能部署到 Android。通常,您需要在 Unity Hub 中下载 SDK 以及 Android 模块。或者,Android SDK 随 Android Studio 一起提供,网址为https://developer.android.com/studio。与构建 iOS 游戏不同,您不需要启动 Unity 之外的任何开发工具 - 您只需在 Unity 中设置指向 AndroidSDK。
Just as you need to install Xcode to deploy to iOS, you need to download the Android SDK to deploy to Android. Usually, you’ll want to download the SDK along with the Android module in Unity Hub. Alternatively, the Android SDK is provided along with Android Studio at https://developer.android.com/studio. Unlike when building an iOS game, you don’t need to fire up any development tools outside of Unity—you simply have to set preferences in Unity that point to the Android SDK.
任何大型软件开发项目将涉及大量复杂的代码文件修订,因此程序员开发了一类称为版本控制系统( VCS ) 的软件来处理此问题。 最流行的免费系统包括 Git ( https://git-scm.com ) 和 Apache Subversion(也称为 SVN,https://subversion.apache.org)。
Any decent-sized software development project will involve a lot of complex revisions to code files, so programmers have developed a class of software called a version-control system (VCS) to handle this problem. A couple of the most popular free systems are Git (https://git-scm.com) and Apache Subversion (also known as SVN, https://subversion.apache.org).
如果你还没有使用 VCS,我强烈建议你开始使用它。Unity 会用临时文件和工作区设置填充项目文件夹,但唯一需要进行版本控制的文件夹是 Assets(确保你的版本控制正在获取 Unity 生成的元文件)、Packages、和项目设置。
If you don’t already use a VCS, I highly recommend starting to use one. Unity fills the project folder with temp files and workspace settings, but the only folders that need to be in version control are Assets (make sure your version control is picking up the meta files generated by Unity), Packages, and ProjectSettings.
虽然Unity 完全能够处理 2D 图形(第 5 章和第 6 章重点介绍 2D 图形),它起源于 3D 游戏引擎,并且继续具有强大的 3D 图形功能。许多 3D 艺术家至少使用过本节中介绍的一款软件包。
Although Unity is perfectly capable of handling 2D graphics (chapters 5 and 6 focus on 2D graphics), it originated as a 3D game engine and continues to have strong 3D graphics features. Many 3D artists work with at least one of the software packages described in this section.
欧特克Maya ( www.autodesk.com/products/maya/overview ) 是一款 3D 艺术和动画软件包,在电影制作方面有着深厚的根基。Maya 的功能集几乎涵盖了 3D 艺术家需要完成的所有任务,从制作精美的电影动画到制作高效的游戏模型。在 Maya 中制作的 3D 动画(例如角色行走)可以导出到统一。
Autodesk Maya (www.autodesk.com/products/maya/overview) is a 3D art and animation package with deep roots in moviemaking. Maya’s feature set covers almost every task that comes up for 3D artists, from crafting beautiful cinematic animations to making efficient game-ready models. 3D animation done in Maya (such as a character walking) can be exported over to Unity.
其他广泛使用的 3D 艺术和动画软件包 Autodesk 3ds Max ( www.autodesk.com/products/3ds-max/overview ) 提供几乎相同的功能集,并且在工作流程上与 Maya 相当。3ds Max 仅在 Windows 上运行(而其他工具,包括 Maya,是跨平台的),但它在游戏中的使用频率与 Maya 相同行业。
Another widely used 3D art and animation package, Autodesk 3ds Max (www.autodesk.com/products/3ds-max/overview) offers an almost identical feature set and is quite comparable in workflow to Maya. 3ds Max runs only on Windows (whereas other tools, including Maya, are cross-platform), but it’s used just as often in the game industry.
尽管Blender ( www.blender.org )在游戏行业中并不像 3ds Max 或 Maya 那样常用,但它与其他应用程序相当。Blender 还涵盖了几乎所有的 3D 艺术任务,最重要的是,Blender 是开源的!鉴于它在所有平台上都是免费的,Blender 是本指南中唯一一款可用的 3D 艺术应用程序书。
Though not as commonly used in the game industry as either 3ds Max or Maya, Blender (www.blender.org) is comparable to those other applications. Blender also covers almost all 3D art tasks and, best of all, Blender is open source! Given that it’s available for free on all platforms, Blender is the only 3D art application that’s assumed to be available by this book.
这易于使用的建模工具特别适合建筑物和建筑元素。与以前的工具不同,SketchUp(www.sketchup.com)不涵盖所有甚至大多数 3D 艺术任务;相反,它专注于使建筑物和其他简单形状的建模变得容易。此工具在游戏开发中非常有用,可用于白盒和等级編輯。
This simple-to-use modeling tool is especially well-suited for buildings and architectural elements. Unlike the previous tools, SketchUp (www.sketchup.com) does not cover all or even most 3D art tasks; instead, it focuses on making it easy to model buildings and other simple shapes. This tool is useful in game development for whiteboxing and level editing.
2D图像对于所有游戏都至关重要,无论是直接显示在 2D 游戏中还是作为 3D 模型表面上的纹理。正如您将在本节中看到的那样,游戏开发中经常会出现几种 2D 图形工具。
2D images are crucial to all games, whether they’re displayed directly for 2D games or as textures on the surface of 3D models. Several 2D graphics tools come up often in game development, as you’ll see in this section.
AdobePhotoshop(www.adobe.com/products/photoshop.html ) 无疑是目前使用最广泛的 2D 图像应用程序。Photoshop 中的工具可用于修饰现有图像、应用图像滤镜,甚至从头开始绘制图片。Photoshop 支持数十种文件格式,包括所有用于统一。
Adobe Photoshop (www.adobe.com/products/photoshop.html) is easily the most widely used 2D image application there is. The tools in Photoshop can be used to touch up existing images, apply image filters, or even paint pictures from scratch. Photoshop supports dozens of file formats, including all image formats used in Unity.
一个GIMP(www.gimp.org )是GNU 图像处理程序的缩写,是最著名的开源 2D 图形应用程序。GIMP 在功能和可用性方面都落后于 Photoshop,但它仍然是一个有用的图像编辑器,而且你无法超越价格!
An acronym for GNU Image Manipulation Program, GIMP (www.gimp.org) is the best-known open source 2D graphics application. GIMP trails Photoshop in both features and usability, but it’s still a useful image editor, and you can’t beat the price!
然而前面提到的工具都用于游戏开发领域之外,TexturePacker 只用于游戏开发。但它在设计它的任务上非常出色:组装用于 2D 游戏的精灵表。如果你正在开发 2D 游戏,你可能想试试 TexturePacker(www.codeandweb.com/texturepacker)。
Whereas the previously mentioned tools are all used beyond the field of game development, TexturePacker is useful only for game development. But it’s very good at the task it was designed for: assembling sprite sheets to use in 2D games. If you’re developing a 2D game, you probably want to try out TexturePacker (www.codeandweb.com/texturepacker).
像素艺术是 2D 游戏艺术风格中最具代表性的风格之一,而 Aseprite ( www.aseprite.org ) 和 Pyxel Edit ( www.pyxeledit.com ) 都是不错的像素艺术工具。Photoshop 从技术上来说也可以用于像素艺术,但它并不专注于这一任务。此外,动画功能在 Aseprite 和 Pyxel Edit 中更为突出。派克斯编辑。
Pixel art is one of the most recognizable 2D gaming art styles, and Aseprite (www.aseprite.org) and Pyxel Edit (www.pyxeledit.com) are good pixel art tools. Photoshop can technically be used for pixel art as well, but it’s not focused on that task. Furthermore, the animation features are more front-and-center at Aseprite and Pyxel Edit.
一个有各种各样的音频制作工具可供选择,包括声音编辑器(处理原始波形)和音序器(使用音符序列创作音乐)。为了让您了解可用的音频软件,本节将介绍两种主要的声音编辑工具。此列表之外的其他示例包括 Logic、Ableton 和 Reason。
A dizzying array of audio production tools is available, including both sound editors (which work with raw waveforms) and sequencers (which compose music using a sequence of notes). To give a taste of the audio software available, this section looks at two major sound-editing tools. Other examples beyond this list include Logic, Ableton, and Reason.
专业工具音频软件 ( www.avid.com/en/pro-tools ) 拥有许多有用的功能,被无数音乐制作人和音频工程师视为行业标准。它经常用于各种专业音频工作,包括游戏发展。
Pro Tools audio software (www.avid.com/en/pro-tools) boasts many useful features and is considered the industry standard by countless music producers and audio engineers. It’s frequently used for all sorts of professional audio work, including game development.
虽然它对于专业音频工作来说远没有那么有用,Audacity ( www.audacityteam.org ) 是一款方便的声音编辑器,可用于小规模的音频工作,例如准备简短的声音文件以用作游戏中的音效。对于那些寻找开源音效編輯软件。
Although it is nowhere near as useful for professional audio work, Audacity (www.audacityteam.org) is a handy sound editor for small-scale audio work, like preparing short sound files to use as sound effects in a game. This is a popular choice for those looking for open source sound-editing software.
在第 2 章中在第 4 章和第 5 章中,我们研究了如何创建具有大面积平坦墙壁和地板的关卡。但是更详细的对象呢?比如说,如果你想要在房间里放置有趣的家具怎么办?你可以通过在外部 3D 艺术应用程序中构建 3D 模型来实现这一点。回想一下第 4 章简介中的定义:3D 模型是游戏中的网格对象(3D 形状)。在本附录中,我将向您展示如何创建简单长凳的网格对象(图 C.1)。
In chapters 2 and 4, we looked at creating levels with large flat walls and floors. But what about more detailed objects? What if you want, say, interesting furniture in the room? You can accomplish that by building 3D models in external 3D art apps. Recall the definition from the introduction to chapter 4: 3D models are the mesh objects in the game (the 3D shapes). In this appendix, I’ll show you how to create a mesh object of a simple bench (figure C.1).
Figure C.1 Diagram of the simple bench you’re going to model.
尽管附录 B 列出了几种 3D 艺术工具,但在本练习中我们将使用 Blender,因为它是开源的,因此所有读者都可以使用。您将在 Blender 中创建一个网格对象,并将其导出到可与 Unity 配合使用的艺术资产。
Although appendix B lists several 3D art tools, we’ll use Blender for this exercise because it’s open source and thus accessible to all readers. You’ll create a mesh object in Blender and export that to an art asset that works with Unity.
提示建模是一个很大的话题,但我们只介绍一些建模功能,这些功能可让您创建工作台。如果您想在本章之后继续学习有关建模的更多信息,请查看有关该主题的许多书籍和教程(首先,请查看www.blender.org上的学习资源)。
TIP Modeling is a huge topic, but we’ll cover only a handful of modeling functions that will allow you to create the bench. If you want to keep learning more about modeling after this chapter, look at some of the many books and tutorials on the subject (to start with, look at the learning resources at www.blender.org).
警告我使用的是 Blender 2.91,因此解释和截图均来自该版本的软件。Blender 经常发布新版本,按钮的位置或命令的名称可能会发生变化。
WARNING I used Blender 2.91, so the explanations and screenshots come from that version of the software. Newer versions of Blender are released frequently, and changes may occur to the placement of buttons or names of commands.
发射Blender 并单击启动画面外部以将其关闭;初始默认屏幕如图 C.2 所示,场景中间有一个立方体。使用鼠标中键来操作相机视图:单击并拖动以翻滚,按住 Shift 并单击并拖动以平移,按住 Ctrl 并单击并拖动以缩放。左键单击相机以将其选中,按住 Shift 并单击灯光以将其选中,然后按 X 删除两者。
Launch Blender and click outside the splash screen to dismiss it; the initial default screen looks like figure C.2, with a cube in the middle of the scene. Use the middle mouse button to manipulate the camera view: click and drag to tumble, Shift with click-drag to pan, and Ctrl with click-drag to zoom. Left-click the camera to select it, hold Shift while clicking the light to select it too, and then press X to delete both.
Figure C.2 The initial default screen in Blender
Blender 以对象模式启动,顾名思义,该模式可让您操纵整个对象,并在场景中移动它们。要详细编辑单个网格对象,您必须选择它并切换到编辑模式;图 C.3 显示了您使用的菜单。
Blender starts out in Object mode, which, as the name implies, enables you to manipulate entire objects, moving them around the scene. To edit a single mesh object in detail, you must select it and switch to Edit mode; figure C.3 shows the menu you use.
Figure C.3 Menu for switching from Object to Edit mode
警告Blender 界面的许多部分都是上下文相关的,此菜单就是其中之一。列出的菜单项取决于所选的对象,无论是网格、相机还是其他对象。
WARNING Many parts of Blender’s interface are context sensitive, and this menu is one. The menu items listed vary depending on which object is selected, be it a mesh, a camera, or something else.
首次切换到编辑模式时,Blender 设置为“顶点选择”模式,但按钮可让您在“顶点”、“边”和“面选择”模式之间切换(参见图 C.4)。各种选择模式允许您选择不同的网格元素。
When you first switch to Edit mode, Blender is set to Vertex Selection mode, but buttons let you switch between Vertex, Edge, and Face Selection modes (refer to figure C.4). The various selection modes allow you to select different mesh elements.
Figure C.4 Controls along the sides of the viewport
定义 网格元素是构成网格几何形状的顶点、边和面 - 换句话说,就是各个角点、连接点的线以及连接线之间填充的形状。
DEFINITION Mesh elements are the vertices, edges, and faces that make up the geometry of the mesh—in other words, the individual corner points, the lines connecting the points, and the shapes filled in between connected lines.
这些是使用 Blender 的基本控件,现在我们将看到一些用于编辑模型的功能。首先,将立方体缩放为长而薄的木板。选择模型的每个顶点(确保也选择对象背对面的顶点;按 A 键选择全部),然后切换到缩放工具。单击并拖动蓝色臂以垂直缩小,然后单击并拖动绿色箭头以横向扩展(参见图 C.5)。
These are the basic controls for using Blender, so now we’ll see some functions for editing the model. To start, scale the cube into a long, thin plank. Select every vertex of the model (be sure to also select vertices on the side of the object facing away; press A to select all) and then switch to the Scale tool. Click-drag the blue arm to scale down vertically, and then click-drag the green arrow to scale out sideways (see figure C.5).
Figure C.5 Mesh scaled into a long, thin plank
切换到“面选择”模式(使用图 C.4 中指示的按钮)并选择木板的两端。您可以单独单击面,并记住在添加到选择时按住 Shift。现在单击视口顶部的“网格”菜单,然后选择“挤出”>“挤出单个面”(参见图 C.6)。当您移动鼠标时,您会看到木板两端添加了其他部分;将它们稍微移出,然后单击左键确认。使这个额外的部分仅与长凳腿的宽度相同,从而为您提供一些额外的几何图形以供使用。
Switch to Face Selection mode (use the button indicated in figure C.4) and select both small ends of the plank. You can click faces individually, and remember to hold Shift when adding to the selection. Now click the Mesh menu at the top of the viewport and choose Extrude > Extrude Individual Faces (see figure C.6). As you move the mouse, you’ll see additional sections added to the ends of the plank; move them out slightly and then left-click to confirm. Make this additional section only the width of the bench legs, giving yourself a little additional geometry to work with.
Figure C.6 In the Mesh menu, use Extrude Individual Faces to pull out extra sections.
定义 挤压推出具有选定面形状的横截面的新几何体。不同的挤压命令定义了选择多个元素时要执行的操作:挤压单个面将每个面视为要挤压的单独部分,而标准挤压面命令将整个选择视为单个部分。
DEFINITION Extrude pushes out new geometry with a cross section in the shape of the selected faces. The different extrude commands define what to do when multiple elements are selected: Extrude Individual Faces treats each face as a separate piece to extrude, whereas the standard Extrude Faces command treats the entire selection as a single piece.
现在查看木板的底部,选择两端的两个薄面。再次使用“挤出单个面”命令拉下长凳的腿(参见图 C.7)。
Now look at the bottom of the plank and select the two thin faces on each end. Use the Extrude Individual Faces command again to pull down legs for the bench (refer to figure C.7).
Figure C.7 Select the thin faces underneath the bench and pull down legs.
形状完成了!但在将模型导出到 Unity 之前,您需要处理纹理模型。
The shape is complete! But before you export the model over to Unity, you want to take care of texturing the model.
3D模型可以在其表面上显示 2D 图像(称为纹理)。对于像墙壁这样的大而平坦的表面,2D 图像与 3D 表面的具体关系很简单:只需将图像拉伸到平坦表面上即可。但是对于形状奇怪的表面,例如长凳的侧面,该怎么办呢?这时,理解纹理坐标的概念就变得很重要。
3D models can have 2D images (referred to as textures) displayed on their surface. How exactly the 2D images relate to the 3D surface is straightforward for a large, flat surface like a wall: simply stretch the image across the flat surface. But what about an oddly shaped surface, like the sides of the bench? This is where it becomes important to understand the concept of texture coordinates.
纹理坐标定义纹理的各部分与网格各部分之间的关系。这些坐标将网格元素分配给纹理的区域。想象一下包装纸(见图 C.8);3D 模型是被包装的盒子,纹理是包装纸,纹理坐标表示包装纸将放在盒子上的点。纹理坐标定义 2D 图像上的点和形状;这些形状与网格上的多边形相关,图像的该部分出现在网格的该部分上。
Texture coordinates define how parts of the texture relate to parts of the mesh. These coordinates assign mesh elements to areas of the texture. Think about it like wrapping paper (see figure C.8); the 3D model is the box being wrapped, the texture is the wrapping paper, and the texture coordinates represent the points on the box where the wrapping paper will go. The texture coordinates define points and shapes on the 2D image; those shapes correlate to polygons on the mesh, and that part of the image appears on that part of the mesh.
Figure C.8 Wrapping paper makes a good analogy for how texture coordinates work.
提示:纹理坐标的另一个名称是UV 坐标。这是因为纹理坐标使用字母 U 和 V 定义,就像 3D 模型上的坐标使用 X、Y 和 Z 定义一样。
TIP Another name for texture coordinates is UV coordinates. This is because texture coordinates are defined using the letters U and V, just as coordinates on the 3D model are defined using X, Y, and Z.
将一个物体的一部分与另一个物体的一部分相关联的技术术语是映射——因此,创建纹理坐标的过程被称为纹理映射。这个过程的另一个名字来自包装纸的类比,叫做展开。还有更多的术语是通过混合其他术语而产生的,比如UV 展开;围绕纹理映射存在许多本质上同义的术语,因此尽量不要混淆。
The technical term for correlating part of one thing to part of another is mapping—hence the term texture mapping for the process of creating texture coordinates. Coming from the wrapping paper analogy, another name for the process is unwrapping. And still more terms are created by mashing up the other terminology, like UV unwrapping; a lot of essentially synonymous terms surrounding texture mapping exist, so try not to get confused.
传统上,纹理映射的过程非常复杂,但幸运的是,Blender 提供了使该过程相当简单的工具。首先,您要在模型上定义接缝;如果您进一步考虑包裹一个盒子(或者更好的是,考虑另一个方向,展开一个盒子),您会意识到,在展开成二维时,并非 3D 形状的每个部分都能保持无缝。在 3D 形式中,侧面分开的地方必须有接缝。Blender 允许您选择边缘并将其声明为接缝。
Traditionally, the process of texture mapping has been wickedly complicated, but fortunately, Blender provides tools to make the process fairly simple. First you define seams on the model; if you think further about wrapping around a box (or better yet, think about the other direction, unfolding a box), you’ll realize that not every part of a 3D shape can remain seamless when unfolded into two dimensions. There will have to be seams in the 3D form where the sides come apart. Blender enables you to select edges and declare them as seams.
切换到“边缘选择”模式(参见图 C.4 中的按钮),然后选择长凳底部外侧的边缘。现在选择“边缘”>“标记接缝”(参见图 C.9)。这告诉 Blender 分离长凳底部以便进行纹理映射。对长凳的侧面执行相同的操作,但不要将侧面完全分开。相反,只缝合长凳腿上的边缘;这样,侧面将保持与长凳的连接,同时像翅膀一样展开。
Switch to Edge Selection mode (see the buttons in figure C.4) and select the edges along the outside of the bottom of the bench. Now choose Edge > Mark Seam (see figure C.9). This tells Blender to separate the bottom of the bench for the purposes of texture mapping. Do the same thing for the sides of the bench, but don’t separate the sides entirely. Instead, seam only the edges running up the legs of the bench; this way, the sides will remain connected to the bench while spreading out like wings.
Figure C.9 Seam edges along the bottom of the bench and along the legs
标记完所有接缝后,运行纹理展开命令。首先,选择整个网格(只需按 A 键选择所有内容,或框选,不要忘记物体背对的一侧)。接下来,选择 UV > 展开以创建纹理坐标。但您无法在此视图中看到纹理坐标;Blender 默认为场景的 3D 视图。切换到 UV 编辑工作区以查看纹理坐标,使用屏幕顶部的工作区选项卡(参见图 C.10)。
Once all the seams are marked, run the texture unwrap command. First, select the entire mesh (just press A to select everything, or box select and don’t forget the side of the object facing away). Next, choose UV > Unwrap to create the texture coordinates. But you can’t see the texture coordinates in this view; Blender defaults to a 3D view of the scene. Switch to the UV Editing workspace to see the texture coordinates, using the workspace tabs at the top of the screen (see figure C.10).
Figure C.10 Switch to UV Editing, then Export UV Layout.
现在您可以看到纹理坐标。您可以看到长凳的多边形平铺、分离并根据您标记的接缝展开。要绘制纹理,您必须在图像编辑程序中看到这些 UV 坐标。再次参考图 C.10,在纹理坐标视口中的 UV 菜单下选择“导出 UV 布局”;将图像保存为 bench.png(此名称稍后在导入 Unity 时也会使用),大小为 256。
Now you can see the texture coordinates. You can see the polygons of the bench laid out flat, separated, and unfolded according to the seams you marked. To paint a texture, you have to see these UV coordinates in your image-editing program. Referring again to figure C.10, choose Export UV Layout under the UV menu in the texture coordinates viewport; save the image as bench.png (this name will also be used later when importing into Unity) with a size of 256.
在图像编辑器中打开此图像,并为纹理的各个部分涂上颜色。为不同的 UV 涂上不同的颜色会为这些面涂上不同的颜色。例如,图 C.11 显示,长凳底部在 UV 布局顶部展开时呈现深蓝色,长凳侧面涂上红色。现在可以将图像带回 Blender 为模型添加纹理;选择“图像”>“打开”。
Open this image in your image editor and paint colors for the various parts of your texture. Painting different colors for different UVs will put different colors on those faces. For example, figure C.11 shows darker blue where the bottom of the bench was unfolded on the top of the UV layout, and red was painted on the sides of the bench. Now the image can be brought back into Blender to texture the model; choose Image > Open.
图 C.11 在导出的 UV 上涂抹颜色,然后将纹理带入 Blender。
Figure C.11 Paint colors over the exported UVs and then bring the texture into Blender.
即使在 UV 编辑视图中打开了纹理图像,您仍然无法在 3D 视图中看到模型上的纹理。这需要几个步骤:将图像分配给对象的材质,然后在视口中打开纹理(参见图 C.12)。现在您可以看到已应用纹理的成品长凳!
Even though the texture image is open in the UV editing view, you still can’t see the texture on the model in the 3D view. That requires a couple more steps: assign the image to the object’s material and then turn on textures in the viewport (see figure C.12). Now you can see the finished bench, with texture applied!
Figure C.12 Set the image on the object’s material to view the texture on the model.
现在保存模型。Blender 将使用 Blender 的原生文件格式,以 .blend 扩展名保存文件。使用原生文件格式,以便正确保留 Blender 的所有功能,但稍后您必须将模型导出为其他文件格式(第 4 章推荐使用 FBX)才能将其导入 Unity。请注意,纹理图像不会保存在模型文件中;保存的是图像的引用,但您仍然需要图像文件存在參考。
Save the model now. Blender will save the file with the .blend extension, using the native file format for Blender. Work in the native file format so that all the features of Blender will be preserved correctly, but later you’ll have to export the model to a different file format (FBX is recommended in chapter 4) to import it into Unity. Note that the texture image isn’t saved in the model file; what’s saved is a reference to the image, but you still need the image file that’s being referenced.
本书是 Unity 游戏开发的完整介绍,但除了这个介绍之外,还有很多东西需要学习。读完这本书后,你会发现网上有很多很棒的资源,可以用来进一步学习。
This book is a complete introduction to game development in Unity, but there’s much more to learn beyond this introduction. You’ll find lots of great resources online that you can use to go further after finishing this book.
许多网站提供有关 Unity 内各种主题的定向信息。其中一些甚至由 Unity 背后的公司正式提供。
Many sites provide directed information on a variety of topics within Unity. Several of these are even provided officially by the company behind Unity.
统一提供了全面的用户手册。它不仅有助于查找信息,而且主题列表本身也有助于全面了解 Unity 的功能。您可以在以下位置找到手册http://docs.unity3d.com/Manual/index.html。
Unity provides a comprehensive user manual. Not only is it useful for looking up information, but the list of topics is useful by itself for providing a full idea of what Unity is capable of. You can find the manual at http://docs.unity3d.com/Manual/index.html.
统一程序员最终会比阅读任何其他资源更频繁地阅读脚本参考(至少我是!)。用户手册涵盖了引擎的功能和编辑器的使用,但脚本参考是对 Unity 编程 API 的详尽参考。每个命令都列在http://docs.unity3d.com/ScriptReference/index.html。
Unity programmers end up reading the scripting reference more than any other resource (at least, I do!). The user manual covers the capabilities of the engine and use of the editor, but the scripting reference is a thorough reference to Unity’s programming API. Every command is listed at http://docs.unity3d.com/ScriptReference/ index.html.
Unity 的官方网站包含几个综合教程,位于“学习”部分。最重要的是,这些教程都是视频。这可能是好事也可能是坏事,取决于你的观点;如果你喜欢看视频教程,https://learn.unity.com是一个不错的网站出去。
Unity’s official website includes several comprehensive tutorials, found in the Learn section. Most importantly, the tutorials are all videos. This may be good or bad, depending on your perspective; if you are someone who likes to watch video tutorials, https://learn.unity.com is a good site to check out.
相当Catlike Coding 不仅引导学习者完成一款完整的游戏,还提供了一系列实用而有趣的主题。这些主题甚至不一定是专门针对游戏开发的,但却是培养 Unity 编程技能的好方法。这些教程可以在以下网址找到catlikecoding.com/unity/tutorials/。
Rather than walking learners through a complete game, Catlike Coding offers a grab bag of useful and interesting topics. The topics aren’t even necessarily about game development specifically, but are a great way to build up programming skills in Unity. The tutorials can be found at catlikecoding.com/unity/tutorials/.
Game development at Stack Exchange
堆Exchange 是另一个很棒的信息网站,其格式与前面列出的网站不同。Stack Exchange 不是提供一系列独立的教程,而是提供以文本为主的问答,鼓励搜索。它包含大量主题的部分,https: //gamedev.stackexchange.com是该网站专注于游戏开发的区域。值得一提的是,我在那里寻找 Unity 信息的频率几乎与使用脚本 r 的频率一样高參考。
Stack Exchange is another great information site with a different format from the previous ones listed. Rather than a series of self-contained tutorials, Stack Exchange presents a mostly text QA that encourages searching. It has sections about a huge array of topics, and https://gamedev.stackexchange.com is the area of the site focused on game development. For what it’s worth, I look for Unity information there almost as often as I use the script reference.
作为如附录 B 所述,外部艺术应用程序是创建视觉效果惊人的游戏的关键部分。有许多资源可以教授 Maya、3ds Max、Blender 或任何其他 3D 艺术应用程序。附录 C 提供了有关 Blender 的教程。一个关于使用 Maya LT(面向游戏开发且价格较低的 Maya 版本)的在线指南是在https://steamcommunity.com/sharedfiles/filedetails/?id=242847724。
As described in appendix B, external art applications are a crucial part of creating visually stunning games. Many resources that teach about Maya, 3ds Max, Blender, or any of the other 3D art applications are available. Appendix C offers a tutorial about Blender. One online guide about using Maya LT (a game development-oriented and less expensive version of Maya) is at https://steamcommunity.com/sharedfiles/filedetails/ ?id=242847724.
虽然前面列出的资源提供了有关 Unity 的教程和/或学习信息,本节中的网站提供了可用于项目的代码。库和插件是另一种对新开发人员有用的资源,不仅可以直接使用,还可以从中学习(通过阅读他们的代码)。
Although the previously listed resources provide tutorials and/or learning information about Unity, the sites in this section provide code that can be used in your projects. Libraries and plugins are another kind of resource that can be useful for new developers, not only for using directly but also for learning from (by reading their code).
Unity Library 是众多开发人员代码贡献的中央数据库,其中托管的脚本涵盖了广泛的功能。该页面的资源部分链接到其他脚本集合。您可以浏览内容网址为https://github.com/UnityCommunity/UnityLibrary。
The Unity Library is a central database of code contributions from many developers, and the scripts hosted there cover a wide range of functionality. The Resources section of that page links to additional script collections. You can browse the content at https://github.com/UnityCommunity/UnityLibrary.
作为在第 3 章中简要提到过,游戏中常用的一种运动效果称为补间。在这种类型的运动中,单个代码命令可以设置对象在一定时间内移动到目标。可以使用 DOTween( http://dotween.demigiant.com)或 LeanTween等库添加补间功能(https://github.com/dentedpixel/LeanTween)。
As mentioned briefly in chapter 3, a kind of motion effect commonly used in games is referred to as a tween. In this type of movement, a single code command can set an object moving to a target over a certain amount of time. Tweening functionality can be added using libraries like DOTween (http://dotween.demigiant.com) or LeanTween (https://github.com/dentedpixel/LeanTween).
这后处理堆栈是一种向游戏添加大量视觉效果(如景深和运动模糊)的简单方法。其中许多效果已集成到一个超级组件中。此包的描述如下http://mng.bz/9aXl。
The Post-Processing Stack is an easy way to add a bunch of visual effects like depth of field and motion blur to your games. Many of these effects have been integrated into one über component. This package is described at http://mng.bz/9aXl.
尽管Unity 的核心已经涵盖了所有游戏平台的各种功能,对于移动游戏,您可能需要安装具有附加功能的软件包。Unity 移动通知包 ( http://mng.bz/jjvx ) 专注于通知,即应用程序在您的设备上生成的小警报电话。
While the core of Unity already covers all sorts of features across all gaming platforms, for mobile games you may want to install packages with additional features. The Unity Mobile Notifications package (http://mng.bz/jjvx) focuses on notifications, the little alerts generated by apps on your phone.
尽管刚刚提到的 Unity 软件包可处理 Android 和 iOS 的本地通知,但它仅支持 iOS 上的远程通知(也称为推送通知)。Android 上的推送通知通过一项名为 Firebase Cloud Messaging 的服务工作,Firebase 的开发者页面(http://mng.bz/WBg0)解释了如何使用其 UnitySDK。
While the Unity package just mentioned handles local notifications for both Android and iOS, it supports remote notifications (also called push notifications) on iOS only. Push notifications on Android work through a service called Firebase Cloud Messaging, and the developer pages for Firebase (http://mng.bz/WBg0) explain how to use its Unity SDK.
Play Games Services from Google
在iOS 上,Unity 内置了 GameCenter 集成,因此您的游戏可以拥有平台原生的排行榜和成就。Android 上的等效系统称为 Google Play Games;尽管它没有内置在 Unity 中,但 Google 维护了一个插件http://mng.bz/80QP。
On iOS, Unity has GameCenter integration built in so that your games can have platform-native leaderboards and achievements. The equivalent system on Android is called Google Play Games; although it isn’t built into Unity, Google maintains a plugin at http://mng.bz/80QP.
这Unity 内置的音频功能非常适合播放录音,但对于高级声音设计工作来说可能会受到限制。FMOD Studio 是一款具有 Unity 插件的高级声音设计工具。查找它在www.fmod.com/studio。
The audio functionality built into Unity works well for playing back recordings, but can be limited for advanced sound design work. FMOD Studio is an advanced sound design tool that has a Unity plugin. Find it at www.fmod.com/studio.
2D游戏演示127
2D game demo 127
building card object and making it react to clicks 110 – 112
使用精灵110构建对象
building object out of sprites 110
点击112显示卡片
revealing card on click 112
displaying various card images 113 – 119
instantiating grid of cards 116 – 117
loading images programmatically 113 – 114
从不可见的SceneController设置图像114 – 116
setting image from invisible SceneController 114 – 116
making and scoring matches 119 – 123
hiding mismatched cards 120 – 121
存储和比较已揭示的卡片120
storing and comparing revealed cards 120
text display for score 121 – 123
从 SceneController 调用 LoadScene 126 – 127
calling LoadScene from SceneController 126 – 127
使用 SendMessage 编程 UIButton 组件124 – 126
programming UIButton component using SendMessage 124 – 126
setting up for 2D graphics 104 – 110
displaying 2D images 107 – 108
项目准备
preparing project 105
switching camera to 2D mode 108 – 110
2D images, texturing scene with 82 – 87
2D interface, advanced 149 – 150
2D 平台游戏146
2D platformer 146
slopes and one-way platforms 140 – 141
jump ability, adding 137 – 140
applying upward impulse 138 – 139
falling from gravity 137 – 138
moving player left and right 132 – 134
碰撞检测134
collision detection 134
编写键盘控件133
writing keyboard controls 133
playing sprite's animation 134 – 137
Mecanim animation system 134 – 136
triggering animations from code 136 – 137
importing sprite sheets 130 – 132
diagramming how basic AI works 60 – 61
tracking character's state 62 – 63
keyboard input component 44 – 49
adjusting components for walking 47 – 49
移动 CharacterController 进行碰撞检测46 – 47
moving CharacterController for collision detection 46 – 47
responding to keypresses 44 – 45
setting rate of movement independent of computer speed 45 – 46
local vs. global coordinate space 35 – 36
可视化运动是如何编程的33
visualizing how movement is programmed 33
writing code to implement diagram 34 – 35
placing objects in scene 27 – 33
player's collider and viewpoint 32 – 33
script component for looking around 37 – 43
horizontal and vertical rotation at same time 41 – 43
horizontal rotation tracking mouse movement 38 – 39
vertical rotation with limits 39 – 41
scripting reactive targets 57 – 60
alerting target that it was hit 58 – 60
determining what was hit 57 – 58
shooting by instantiating prefabs 68 – 74
creating projectile prefab 68 – 69
shooting projectile and colliding with target 70 – 72
adding visual indicators for aiming and hits 54 – 57
ScreenPointToRay command 52 – 54
spawning enemy prefabs 64 – 68
创建敌人预制件65
creating enemy prefab 65
从不可见的 SceneController 实例化65 – 68
instantiating from invisible SceneController 65 – 68
预制件,定义64
prefabs, defined 64
understanding 3D coordinate space 25 – 27
exporting and importing 92 – 94
acceleration, in third-person 3D game 185 – 186
动作关键字240
Action keyword 240
动作角色扮演游戏 (RPG) 演示313
action role-playing game (RPG) demo 313
building by repurposing projects 283 – 298
assembling assets and code from multiple projects 284 – 286
bringing over AI enemy 285 – 286
implementing inventory pop-up 295 – 298
operating devices by using mouse 290 – 292
programming point-and-click controls 286 – 292
replacing old GUI with new interface 292 – 298
设置场景287的俯视图
setting up top-down view of scene 287
updating managers framework 284 – 285
writing movement code 287 – 290
developing overarching game structure 299 – 307
到达303 – 305 号出口即可完成关卡
completing level by reaching exit 303 – 305
controlling mission flow and multiple levels 299 – 303
losing level when caught by enemies 305 – 307
separate scenes for Startup and Level 301 – 303
handling player progression through game 307 – 313
beating game by completing three levels 311 – 313
saving and loading progress 307 – 311
动作类型239
Action type 239
Activate() 函数209
Activate() function 209
Activate() 方法210
Activate() method 210
AddComponentMenu 属性48
AddComponentMenu attribute 48
AddForce() 命令139
AddForce() command 139
AddItem() 方法218
AddItem() method 218
添加剂98
additive 98
advanced 2D interface 149 – 150
diagramming how basic AI works 60 – 61
tracking character's state 62 – 63
bringing enemies to action RPG demo 285 – 286
aiming, visual indicators for 54 – 57
阿尔法通道83
alpha channel 83
锚点156
anchors 156
setting up build tools 329 – 331
Android SDK 357
Android SDK 357
on player character, in third-person 3D game 190 – 198
creating animator controller 194 – 197
defining animation clips in imported model 192 – 194
writing code that operates animator 197 – 198
sprite, in 2D platformer 134 – 137
Mecanim animation system 134 – 136
triggering animations from code 136 – 137
AR Foundation for mobile augmented reality 342 – 348
作为关键字274
as keyword 274
资产
assets 64
atmosphere controlled by code, setting up 232 – 235
大胆359
Audacity 359
音频281
audio 281
audio control interface 265 – 272
setting up central AudioManager 265 – 267
background music, adding 272 – 280
controlling music volume separately 276 – 278
fading between songs 278 – 280
importing sound effects 258 – 261
importing audio files 260 – 261
supported file formats 258 – 260
playing sound effects 261 – 265
assigning looping sound 263 – 264
triggering sound effects from code 264 – 265
音频监听器263
audio listener 263
音频对象271
Audio object 271
音频软件359
audio software 359
音频源262
audio source 262
AudioListener 类269
AudioListener class 269
AudioListener 组件263
AudioListener component 263
setting up AudioSource for 273 – 274
writing code to play audio clips in 274 – 275
AudioManager 属性277
AudioManager property 277
AudioSource 组件263
AudioSource component 263
AudioSource 对象280
AudioSource object 280
AudioSource 变量280
AudioSource variable 280
augmented reality (AR), mobile 342 – 348
Awake() 方法217
Awake() method 217
controlling music volume separately 276 – 278
fading between songs 278 – 280
adding music controls to UI 275 – 276
导入音频片段273
importing audio clips 273
setting up AudioSource 273 – 274
编写代码以在 AudioManager 中播放音频剪辑274 – 275
writing code to play audio clips in AudioManager 274 – 275
烘焙阴影176
baking shadows 176
billboard, networked 247 – 253
caching downloaded image for reuse 251 – 253
displaying images on billboard 250 – 251
loading images from internet 247 – 250
一般性讨论
general discussion 358
building mesh geometry 361 – 364
texture-mapping model 365 – 368
broadcast messenger system 166 – 170
广播和收听 HUD 168 – 170的事件
broadcasting and listening for events from HUD 168 – 170
广播并监听场景167 – 168中的事件
broadcasting and listening for events from scene 167 – 168
integrating event system 166 – 167
浏览器,在322 – 325中与 JavaScript 进行通信
browser, communicating with JavaScript in 322 – 325
缓存251
cache 251
caching downloaded image for reuse 251 – 253
回调240
callback 240
回调参数240
callback parameter 240
回调,理解240
callback, understanding 240
相机组件53
Camera component 53
相机对象30
Camera object 30
camera-relative movement controls 180 – 184
moving forward in that direction 183 – 184
rotating character to face movement direction 180 – 183
adjusting view for third-person 173 – 179
adding shadows to scene 175 – 177
importing character to look at 174 – 175
orbiting camera around player character 177 – 179
controlling in 2D platformer 145 – 146
switching to 2D mode 108 – 110
画布对象295
Canvas object 295
canvas, creating for GUI 151 – 153
card_back 精灵110
card_back sprite 110
CardRevealed() 方法119
CardRevealed() method 119
building card object and making it react to clicks 110 – 112
使用精灵110构建对象
building object out of sprites 110
点击112显示卡片
revealing card on click 112
displaying various images 113 – 119
instantiating grid of cards 116 – 117
loading images programmatically 113 – 114
从不可见的SceneController设置图像114 – 116
setting image from invisible SceneController 114 – 116
hiding mismatched cards 120 – 121
存储和比较已揭示的卡片120
storing and comparing revealed cards 120
笛卡尔坐标系25
Cartesian coordinate system 25
ChangeHealth() 方法225
ChangeHealth() method 225
animations on, in third-person 3D game 190 – 198
creating animator controller 194 – 197
defining animation clips in imported model 192 – 194
writing code that operates animator 197 – 198
importing for third-person game 174 – 175
orbiting camera around 177 – 179
rotating to face movement direction 180 – 183
Clamp() 方法40
Clamp() method 40
点击,显示记忆游戏112上的卡片
click, revealing card on in Memory game 112
CollectibleItem 脚本218
CollectibleItem script 218
collecting items scattered around level 210 – 212
collection object, storing inventory in 217 – 220
collider, player, in 3D demo 32 – 33
colliding with target, in 3D demo 70 – 72
2D 平台游戏134
in 2D platformer 134
移动 CharacterController 为46 – 47
moving CharacterController for 46 – 47
collecting items scattered around level 210 – 212
colliding with physics-enabled obstacles 206 – 207
operating door with trigger object 207 – 210
颜色对象206
Color object 206
color-changing monitor 205 – 206
组件5
components 5
computer speed, setting rate of movement independent of 45 – 46
console games developed in Unity 7 – 8
控制台选项卡16
Console tab 16
ConsumeItem() 方法225
ConsumeItem() method 225
ContainsKey() 方法220
ContainsKey() method 220
控制器212
controller 212
控制器对象295
Controller object 295
协程,使用238 – 242的 HTTP 请求
coroutines, HTTP requests using 238 – 242
回调240
callback 240
coroutine methods cascading through each other 239 – 240
making use of networking code 241 – 242
交叉乘积182
cross product 182
CSG(构造立体几何)80
CSG (constructive solid geometry) 80
立方体物体30
Cube object 30
剔除遮罩177
culling mask 177
光标设置157
Cursor settings 157
damaging player, in first-person shooter game 73 – 74
DataManager 脚本308
DataManager script 308
Deactivate() 函数209
Deactivate() function 209
Deactivate() 方法210
Deactivate() method 210
调试语句120
Debug statement 120
Debug.Log() 命令21
Debug.Log() command 21
依赖项块244
dependencies block 244
向玩家部署游戏348
deploying games to players 348
building for desktop 317 – 320
adjusting player settings 318 – 319
building application 317 – 318
platform-dependent compilation 319 – 320
setting up build tools 326 – 331
在浏览器中与 JavaScript 进行通信322 – 325
communicating with JavaScript in browser 322 – 325
game embedded in web page 321 – 322
developing extended reality 341 – 348
AR Foundation for mobile augmented reality 342 – 348
supporting virtual reality headsets 341 – 342
反序列化245
deserialization 245
building for desktop 317 – 320
adjusting player settings 318 – 319
building application 317 – 318
platform-dependent compilation 319 – 320
built with Unity, examples of 7 – 8
Destroy()方法68
Destroy()method 68
开发人员4
developer 4
DeviceOperator 脚本204, 250, 291
DeviceOperator script 204, 250, 291
DeviceTrigger 脚本210
DeviceTrigger script 210
Dictionary, storing inventory in 217 – 220
定向灯30
directional lights 30
318号指令
directive 318
距离属性62
distance property 62
DllImport 命令323
DllImport command 323
DontDestroyOnLoad() 函数312
DontDestroyOnLoad() function 312
DontDestroyOnLoad() 方法127, 301
DontDestroyOnLoad() method 127, 301
DoorOpenDevice 脚本209
DoorOpenDevice script 209
checking distance and facing before opening 203 – 205
equipping key to use on locked 223 – 224
operating with trigger object 207 – 210
that open and close on keypress, creating 201 – 203
点积190
dot product 190
DOTween 370
DOTween 370
downloading weather data 235 – 247
changing scene based on weather data 246 – 247
requesting HTTP data using coroutines 238 – 242
DrawLine() 命令143
DrawLine() command 143
DrawRay() 命令143
DrawRay() command 143
动态字符串165
dynamic string 165
edges, handling in third-person 3D game 186 – 190
effects, creating using particle systems 95 – 100
调整默认效果的参数96
adjusting parameters on default effect 96
applying new texture for fire 98 – 99
attaching particle effects to 3D objects 99 – 100
diagramming how basic AI works 60 – 61
tracking character's state 62 – 63
scripting reactive targets 57 – 60
alerting target that it was hit 58 – 60
determining what was hit 57 – 58
shooting by instantiating prefabs 68 – 74
creating projectile prefab 68 – 69
shooting projectile and colliding with target 70 – 72
spawning enemy prefabs 64 – 68
创建敌人预制件65
creating enemy prefab 65
从不可见的 SceneController 实例化65 – 68
instantiating from invisible SceneController 65 – 68
预制件,定义64
prefabs, defined 64
bringing from 3D game 285 – 286
被305 – 307抓住时失去等级
losing level when caught by 305 – 307
EquipItem() 方法223
EquipItem() method 223
equippedItem 属性223
equippedItem property 223
ETC(爱立信纹理压缩)332
ETC (Ericsson Texture Compression) 332
events, updating game by responding to 166 – 170
广播和收听 HUD 168 – 170的事件
broadcasting and listening for events from HUD 168 – 170
广播并监听场景167 – 168中的事件
broadcasting and listening for events from scene 167 – 168
integrating event system 166 – 167
EventTrigger 组件298
EventTrigger component 298
exit, completing level by reaching 303 – 305
exporting custom 3D models 92 – 94
extended reality (XR), developing 341 – 348
AR Foundation for mobile augmented reality 342 – 348
supporting virtual reality headsets 341 – 342
external software tools 356 – 359
音频软件359
audio software 359
挤压364
extrude 364
fading between songs 278 – 280
supported for sound effects 258 – 260
File.Create() 方法310
File.Create() method 310
fire effect, creating using particle systems 95 – 100
调整默认效果的参数96
adjusting parameters on default effect 96
applying new texture for fire 98 – 99
attaching particle effects to 3D objects 99 – 100
火球物体69
Fireball object 69
Firebase 云消息传递371
Firebase Cloud Messaging 371
first-person controls, in 3D demo 44 – 49
adjusting components for walking 47 – 49
移动 CharacterController 进行碰撞检测46 – 47
moving CharacterController for collision detection 46 – 47
responding to keypresses 44 – 45
setting rate of movement independent of computer speed 45 – 46
第一人称射击游戏 (FPS) 演示74
first-person shooter (FPS) game demo 74
diagramming how basic AI works 60 – 61
tracking character's state 62 – 63
scripting reactive targets 57 – 60
alerting target that it was hit 58 – 60
determining what was hit 57 – 58
shooting by instantiating prefabs 68 – 74
creating projectile prefab 68 – 69
shooting projectile and colliding with target 70 – 72
adding visual indicators for aiming and hits 54 – 57
ScreenPointToRay command 52 – 54
spawning enemy prefabs 64 – 68
创建敌人预制件65
creating enemy prefab 65
从不可见的 SceneController 实例化65 – 68
instantiating from invisible SceneController 65 – 68
预制件,定义64
prefabs, defined 64
地板物体30
Floor object 30
laying out primitives according to 80 – 81
FMOD 工作室371
FMOD Studio 371
FPS(第一人称射击游戏)24
FPS (first-person shooter) 24
第17帧
frame 17
帧速率相关45
frame-rate dependent 45
帧速率独立45
frame-rate independent 45
freezeRotation 属性42
freezeRotation property 42
FSM(有限状态机)63
FSM (finite-state machine) 63
game state, managing 212 – 220
programming game managers 214 – 217
setting up player and inventory managers 212 – 213
storing inventory in collection object 217 – 220
游戏对象类型67
GameObject type 67
获取命令165
get command 165
获取函数269
get function 269
GetAxis() 方法39
GetAxis() method 39
GetButtonDown() 函数186
GetButtonDown() function 186
GetItemCount() 方法221
GetItemCount() method 221
GetItemList() 方法221
GetItemList() method 221
GIMP 358
GIMP 358
Google Play 游戏371
Google Play Games 371
图形100
graphics 100
exporting and importing 92 – 94
generating sky visuals by using texture images 87 – 90
creating new skybox material 88 – 90
导入 UI 图像151
importing UI images 151
particle systems, creating effects using 95 – 100
调整默认效果的参数96
adjusting parameters on default effect 96
applying new texture for fire 98 – 99
attaching particle effects to 3D objects 99 – 100
texturing scene with 2D images 82 – 87
drawing floor plan for level 79 – 80
解释79
explained 79
laying out primitives according to plan 80 – 81
重力变量47
gravity variable 47
gravity, in 2D platformer 137 – 138
grid of cards, Memory game demo 116 – 117
modifying in third-person 3D game 186 – 190
接地变量140
grounded variable 140
GUI(图形用户界面)170
GUI (graphical user interface) 170
adding music controls to 275 – 276
定义121
defined 121
immediate mode GUI vs. advanced 2D interface 149 – 150
导入 UI 图像151
importing UI images 151
规划布局150
planning layout 150
programming interactivity in 157 – 166
creating pop-up window 160 – 163
invisible UIController 158 – 160
setting values using sliders and input fields 163 – 166
buttons, images, and text labels 153 – 155
controlling position of elements 156 – 157
creating canvas for interface 151 – 153
updating game by responding to events 166 – 170
广播和收听 HUD 168 – 170的事件
broadcasting and listening for events from HUD 168 – 170
广播并监听场景167 – 168中的事件
broadcasting and listening for events from scene 167 – 168
health, restoring player 224 – 225
hiding mismatched cards, Memory game demo 120 – 121
层次结构选项卡15
Hierarchy tab 15
alerting target that it was hit 58 – 60
determining what was hit 57 – 58
HMD(头戴式显示器)342
HMD (head-mounted display) 342
horizontal movement, in 2D platformer 132 – 134
碰撞检测134
collision detection 134
编写键盘控件133
writing keyboard controls 133
and vertical rotation at same time 41 – 43
tracking mouse movement 38 – 39
HTTP(超文本传输协议)230
HTTP (Hypertext Transfer Protocol) 230
HUD(抬头显示器)148
HUD (heads-up display) 148
Hurt() 方法73
Hurt() method 73
使用协程的超文本传输协议 (HTTP) 请求238 – 242
Hypertext Transfer Protocol (HTTP) requests using coroutines 238 – 242
回调240
callback 240
Hypertext Transfer Protocol (HTTP) requests (continued)
coroutine methods cascading through each other 239 – 240
图标阵列298
icons array 298
IDE(集成开发环境)5
IDE (integrated development environment) 5
if statement 67, 186, 224, 289
IGameManager interface 215, 284
图像变量113
image variable 113
card, displaying various in Memory game 113 – 119
instantiating grid of cards 116 – 117
loading images programmatically 113 – 114
从不可见的SceneController设置图像114 – 116
setting image from invisible SceneController 114 – 116
caching downloaded image for reuse 251 – 253
displaying images on billboard 250 – 251
loading images from internet 247 – 250
音频片段273
audio clips 273
importing audio files 260 – 261
supported file formats 258 – 260
UI 图像151
UI images 151
冲动139
impulse 139
Initialize() 函数334
Initialize() function 334
输入类别39
Input class 39
input fields, pop-up window 163 – 166
Input.GetMouseButtonDown() 方法53
Input.GetMouseButtonDown() method 53
检查器选项卡15
Inspector tab 15
实例化65
instantiate 65
instantiating grid of cards, Memory game demo 116 – 117
from invisible SceneController 65 – 68
creating projectile prefab 68 – 69
shooting projectile and colliding with target 70 – 72
int 参数275
int parameter 275
互动设备和物品226
interactive devices and items 226
collecting items scattered around level 210 – 212
colliding with physics-enabled obstacles 206 – 207
operating door with trigger object 207 – 210
creating doors and other devices 201 – 206
checking distance and facing before opening door 203 – 205
按下201 – 203键即可打开和关闭的门
doors that open and close on keypress 201 – 203
operating color-changing monitor 205 – 206
displaying inventory items in UI 220 – 222
equipping key to use on locked doors 223 – 224
restoring player health 224 – 225
managing inventory data and game state 212 – 220
programming game managers 214 – 217
setting up player and inventory managers 212 – 213
storing inventory in collection object 217 – 220
operating devices by using mouse 290 – 292
interactivity in GUI, programming 157 – 166
creating pop-up window 160 – 163
invisible UIController 158 – 160
setting values using sliders and input fields 163 – 166
互联网,将游戏连接到256
internet, connecting game to 256
downloading weather data 235 – 247
changing scene based on weather data 246 – 247
HTTP requests using coroutines 238 – 242
networked billboard, adding 247 – 253
caching downloaded image for reuse 251 – 253
displaying images on billboard 250 – 251
loading images from internet 247 – 250
outdoor scene, creating 231 – 235
使用天空盒231 – 232生成天空视觉效果
generating sky visuals by using skybox 231 – 232
setting up atmosphere controlled by code 232 – 235
posting data to web server 253 – 256
server-side code in PHP 255 – 256
tracking current weather 254 – 255
inventory data, managing 212 – 220
programming game managers 214 – 217
setting up player and inventory managers 212 – 213
storing inventory in collection object 217 – 220
inventory pop-up, action RPG demo 295 – 298
库存弹出对象295
Inventory Popup object 295
displaying inventory items in 220 – 222
equipping key to use on locked doors 223 – 224
restoring player health 224 – 225
InventoryManager 脚本217
InventoryManager script 217
InventoryPopup 脚本295
InventoryPopup script 295
setting up build tools 326 – 329
IPA(iOS应用程序包)326
IPA (iOS application package) 326
isGrounded 属性186
isGrounded property 186
JavaScript, communicating with in browser 322 – 325
JSON(JavaScript 对象表示法),下载天气数据时解析243 – 246
JSON (JavaScript Object Notation), parsing when downloading weather data 243 – 246
JSON Linq 245
JSON Linq 245
JsonConvert.DeserializeObject 命令245
JsonConvert.DeserializeObject command 245
JsonUtility 类244
JsonUtility class 244
jump ability, adding to 2D platformer 137 – 140
applying upward impulse 138 – 139
falling from gravity 137 – 138
jump action, third-person 3D game 184 – 190
applying vertical speed and acceleration 185 – 186
modifying ground detection to handle edges and slopes 186 – 190
key, to use on locked doors 223 – 224
键盘控制,2D 平台游戏133
keyboard controls, 2D platformer 133
keyboard input component, 3D demo 44 – 49
adjusting components for walking 47 – 49
移动 CharacterController 进行碰撞检测46 – 47
moving CharacterController for collision detection 46 – 47
responding to keypresses 44 – 45
setting rate of movement independent of computer speed 45 – 46
keyboard use with Unity 14 – 15
Knuth shuffle 算法118
Knuth shuffle algorithm 118
标签数组298
labels array 298
lambda 函数252
lambda function 252
LateUpdate() 函数178
LateUpdate() function 178
LateUpdate() 方法145
LateUpdate() method 145
布局、规划 GUI 150
layout, planning GUI 150
延迟加载272
lazy loading 272
LeanTween 370
LeanTween 370
关卡设计79
level design 79
关卡设计师79
level designer 79
beating game by completing three 311 – 313
到达303 – 305 号出口即可完成
completing by reaching exit 303 – 305
controlling mission flow and 299 – 303
losing when caught by enemies 305 – 307
collecting items scattered around 210 – 212
光照贴图176
lightmaps 176
线性插值182
linear interpolation 182
List object, storing inventory in 217 – 220
loading images programmatically 113 – 114
loading player progress 307 – 311
LoadScene 方法,从 SceneController 126 – 127调用
LoadScene method, calling from SceneController 126 – 127
LoadScene() 方法300
LoadScene() method 300
locked doors, equipping key to use on 223 – 224
LookAt() 方法179
LookAt() method 179
looking around, script component for 37 – 43
horizontal and vertical rotation at same time 41 – 43
horizontal rotation tracking mouse movement 38 – 39
vertical rotation with limits 39 – 41
LookRotation() 值182
LookRotation() value 182
looping sound, assigning 263 – 264
循环,定义193
loops, defined 193
loops, playing music 272 – 276
adding music controls to UI 275 – 276
导入音频片段273
importing audio clips 273
setting up AudioSource 273 – 274
编写代码以在 AudioManager 中播放音频剪辑274 – 275
writing code to play audio clips in AudioManager 274 – 275
被敌人抓住时会降低等级,动作角色扮演游戏演示305 – 307
losing level when caught by enemies, action RPG demo 305 – 307
经理的经理
manager of managers 212
managers framework, updating for action RPG demo 284 – 285
ManagerStatus 脚本214
ManagerStatus script 214
Mecanim animation system 134 – 136
记忆游戏演示127
Memory game demo 127
building card object and making it react to clicks 110 – 112
使用精灵110构建对象
building object out of sprites 110
点击112显示卡片
revealing card on click 112
displaying various card images 113 – 119
instantiating grid of cards 116 – 117
loading images programmatically 113 – 114
从不可见的SceneController设置图像114 – 116
setting image from invisible SceneController 114 – 116
making and scoring matches 119 – 123
hiding mismatched cards 120 – 121
存储和比较已揭示的卡片120
storing and comparing revealed cards 120
text display for score 121 – 123
从 SceneController 调用 LoadScene 126 – 127
calling LoadScene from SceneController 126 – 127
使用 SendMessage 编程 UIButton 组件124 – 126
programming UIButton component using SendMessage 124 – 126
setting up for 2D graphics 104 – 110
displaying 2D images 107 – 108
项目准备
preparing project 105
switching camera to 2D mode 108 – 110
MemoryCard 脚本113
MemoryCard script 113
网格单元362
mesh elements 362
网格对象13
mesh object 13
mesh object, creating in Blender 360 – 368
building mesh geometry 361 – 364
texture-mapping model 365 – 368
mismatched cards, Memory game demo 120 – 121
mission flow, action RPG demo 299 – 303
MMO(大型多人在线)229
MMO (massively multiplayer online) 229
MMORPG(大型多人在线角色扮演游戏)229
MMORPGs (MMO role-playing games) 229
移动325
mobile 325
mobile augmented reality 342 – 348
setting up build tools 326 – 331
built with Unity, examples of 8 – 9
移动通知包371
Mobile Notifications package 371
76型
model 76
Blender 360 – 368中的建模工作台
modeling bench in Blender 360 – 368
building mesh geometry 361 – 364
texture-mapping model 365 – 368
MonoBehaviour 类30
MonoBehaviour class 30
operating devices by using 290 – 292
scene navigation using 353 – 354
mouse input code, Memory game demo 111 – 112
老鼠采摘52
mouse picking 52
horizontal and vertical rotation at same time 41 – 43
horizontal rotation tracking mouse movement 38 – 39
vertical rotation with limits 39 – 41
Move() 方法47
Move() method 47
adjusting components for walking 47 – 49
local vs. global coordinate space 35 – 36
移动 CharacterController 进行碰撞检测46 – 47
moving CharacterController for collision detection 46 – 47
programming camera-relative controls 180 – 184
moving forward in that direction 183 – 184
rotating character to face movement direction 180 – 183
responding to keypresses 44 – 45
setting rate of, independent of computer speed 45 – 46
可视化运动是如何编程的33
visualizing how movement is programmed 33
writing code to implement diagram 34 – 35
运动脚本134
movement script 134
moving platforms, in 2D platformers 142 – 144
MovingPlatform 脚本143
MovingPlatform script 143
navigation using mouse 353 – 354
caching downloaded image for reuse 251 – 253
displaying images on billboard 250 – 251
loading images from internet 247 – 250
networking code, making use of 241 – 242
网络服务类236
NetworkService class 236
NetworkService 对象235
NetworkService object 235
NetworkService 脚本238
NetworkService script 238
正常财产189
normal property 189
规范化向量204
normalized vectors 204
正常175
normals 175
可空值289
nullable values 289
对象类型67
Object type 67
ObjectiveTrigger 脚本305
ObjectiveTrigger script 305
objects, placing in scene 27 – 33
player's collider and viewpoint 32 – 33
OnControllerColliderHit() 函数190
OnControllerColliderHit() function 190
OnDisable() 方法168
OnDisable() method 168
OnDrawGizmos() 方法143
OnDrawGizmos() method 143
one-way platforms, in 2D platformers 140 – 141
OnEnable() 方法168
OnEnable() method 168
online learning resources 369 – 371
additional tutorials 369 – 370
OnMouseDown() 函数112
OnMouseDown() function 112
OnMouseSomething 函数125
OnMouseSomething function 125
OnSpeedValue() 方法165
OnSpeedValue() method 165
OnSubmitName() 函数165
OnSubmitName() function 165
OnTriggerEnter() method 72, 209
OnTriggerExit() 方法209
OnTriggerExit() method 209
操作函数206
Operate function 206
OrbitCamera 组件301
OrbitCamera component 301
orbiting camera around player character 177 – 179
正交定义109
orthographic, defined 109
outdoor scene, creating 231 – 235
changing scene based on weather data 246 – 247
使用天空盒231 – 232生成天空视觉效果
generating sky visuals by using skybox 231 – 232
setting up atmosphere controlled by code 232 – 235
OverlapSphere() 方法204
OverlapSphere() method 204
参数136
parameters 136
解析242
parsing 242
JSON when downloading weather data 243 – 246
XML when downloading weather data 242 – 243
particle systems, creating effects using 95 – 100
调整默认效果的参数96
adjusting parameters on default effect 96
applying new texture for fire 98 – 99
attaching particle effects to 3D objects 99 – 100
PCM(脉冲编码调制)261
PCM (pulse code modulation) 261
Photoshop 358
Photoshop 358
PHP, server-side code in 255 – 256
physics-enabled obstacles, colliding with 206 – 207
Physics.Raycast() 方法52
Physics.Raycast() method 52
Physics.SphereCast() 方法62
Physics.SphereCast() method 62
像素完美109
pixel-perfect 109
platform-dependent compilation 319 – 320
PlatformerPlayer 脚本136
PlatformerPlayer script 136
玩家对象30
Player object 30
Player Settings, Build Settings window 318 – 319
玩家标签291
Player tag 291
PlayerCharacter 脚本73
PlayerCharacter script 73
PlayerManager 脚本217
PlayerManager script 217
collider and viewpoint, in 3D demo 32 – 33
damaging in first-person shooter game 73 – 74
handling progression through action RPG 307 – 313
beating game by completing three levels 311 – 313
saving and loading progress 307 – 311
moving left and right in 2D platformer 132 – 134
碰撞检测134
collision detection 134
编写键盘控件133
writing keyboard controls 133
adding music controls to UI 275 – 276
导入音频片段273
importing audio clips 273
setting up AudioSource 273 – 274
编写代码以在 AudioManager 中播放音频剪辑274 – 275
writing code to play audio clips in AudioManager 274 – 275
playing sound effects 261 – 265
assigning looping sound 263 – 264
triggering sound effects from code 264 – 265
PlayMusic() 方法274
PlayMusic() method 274
PlayOneShot() 方法265
PlayOneShot() method 265
plugins for mobile games 332 – 340
点光源30
point lights 30
point-and-click controls for action RPG demo 286 – 292
inventory, action RPG demo 295 – 298
setting values using sliders and input fields 163 – 166
positioning objects on GUI 156 – 157
后处理堆栈370
Post-Processing Stack 370
posting data to web server 253 – 256
server-side code in PHP 255 – 256
tracking current weather 254 – 255 – 318
shooting by instantiating 68 – 74
creating projectile prefab 68 – 69
shooting projectile and colliding with target 70 – 72
创建敌人预制件65
creating enemy prefab 65
从不可见的 SceneController 实例化65 – 68
instantiating from invisible SceneController 65 – 68
预制件,定义64
prefabs, defined 64
primitives, laying out according to floor plan 80 – 81
Pro Tools 359
Pro Tools 359
程序员4
programmer 4
external software tools 356 – 357
运动,可视化33
of movement, visualizing 33
项目选项卡16
Project tab 16
projectile prefab, creating 68 – 69
PVRTC(PowerVR纹理压缩)332
PVRTC (PowerVR Texture Compression) 332
半径变量204
radius variable 204
射线52
ray 52
射线对象54
Ray object 54
Raycast() 方法54
Raycast() method 54
adding visual indicators for aiming and hits 54 – 57
ScreenPointToRay command 52 – 54
RayShooter 类56
RayShooter class 56
reactive targets, scripting 57 – 60
alerting target that it was hit 58 – 60
determining what was hit 57 – 58
ReactiveTarget 脚本59
ReactiveTarget script 59
ReactToHit()方法59
ReactToHit()method 59
矩形工具14
Rect tool 14
RelativeMovement 组件288
RelativeMovement component 288
RelativeMovement script 183, 203
渲染,定义57
rendering, defined 57
RequireComponent attribute 48, 184
requireKey 布尔值223
requireKey Boolean 223
requireKey 选项224
requireKey option 224
Resources.Load() 命令272
Resources.Load() command 272
Resources.Load() 方法221
Resources.Load() method 221
restart button, Memory game demo 123 – 127
从 SceneController 调用 LoadScene 126 – 127
calling LoadScene from SceneController 126 – 127
使用 SendMessage 编程 UIButton 组件124 – 126
programming UIButton component using SendMessage 124 – 126
Restart() 方法126
Restart() method 126
保留模式149
retained mode 149
揭示卡片,记忆游戏演示120
revealed cards, Memory game demo 120
骑手356
Rider 356
根对象29
root object 29
旋转命令39
Rotate command 39
Rotate() 方法35
Rotate() method 35
rotating character to face movement direction 180 – 183
rotation, responding to mouse input 37 – 43
horizontal and vertical rotation at same time 41 – 43
horizontal rotation tracking mouse movement 38 – 39
vertical rotation with limits 39 – 41
RPG(角色扮演游戏)283
RPG (role-playing game) 283
在 Unity 17 – 18中运行代码
saving player progress 307 – 311
broadcasting and listening for events from 167 – 168
适用于 RPG 演示301 – 303中的启动和级别
for startup and levels in action RPG demo 301 – 303
navigation using mouse 353 – 354
自上而下视角动作 RPG 演示287
top-down view in action RPG demo 287
calling LoadScene from 126 – 127
instantiating enemy prefab from 65 – 68
setting card image from 114 – 116
SceneController 组件113
SceneController component 113
SceneController 对象126
SceneController object 126
SceneController 脚本67
SceneController script 67
placing in 2D platformer 129 – 130
分数变量123
score variable 123
score, Memory game demo 121 – 123
scoreLabel 变量123
scoreLabel variable 123
屏幕空间—相机设置152
Screen Space—Camera setting 152
屏幕空间—叠加设置152
Screen Space—Overlay setting 152
ScreenPointToRay command 52 – 54
ScreenPointToRay() 方法52
ScreenPointToRay() method 52
horizontal and vertical rotation at same time 41 – 43
horizontal rotation tracking mouse movement 38 – 39
vertical rotation with limits 39 – 41
脚本参考369
scripting reference 369
adjusting components for walking 47 – 49
local vs. global coordinate space 35 – 36
移动 CharacterController 进行碰撞检测46 – 47
moving CharacterController for collision detection 46 – 47
responding to keypresses 44 – 45
setting rate of movement independent of computer speed 45 – 46
可视化运动是如何编程的33
visualizing how movement is programmed 33
writing code to implement diagram 34 – 35
alerting target that it was hit 58 – 60
determining what was hit 57 – 58
SendMessage,使用124 – 126编程 UIButton 组件
SendMessage, programming UIButton component using 124 – 126
SendMessage() 方法126
SendMessage() method 126
SerializeField 属性66
SerializeField attribute 66
server, posting data to 253 – 256
server-side code in PHP 255 – 256
tracking current weather 254 – 255
设置函数269
set function 269
SetActive() 方法112
SetActive() method 112
SetFloat() 方法233
SetFloat() method 233
SetOvercast() 方法246
SetOvercast() method 246
着色器88
shader 88
shadows, adding to third-person scene 175 – 177
by instantiating prefabs 68 – 74
creating projectile prefab 68 – 69
shooting projectile and colliding with target 70 – 72
adding visual indicators for aiming and hits 54 – 57
ScreenPointToRay command 52 – 54
shuffling cards, Memory game demo 118 – 119
骨骼动画191
skeletal animation 191
SketchUp 358
SketchUp 358
generating by using texture images 87 – 90
creating new skybox material 88 – 90
generating using skybox 231 – 232
creating new skybox material 88 – 90
generating sky visuals using 231 – 232
slerp(球面线性插值)182
slerp (spherical linear interpolation) 182
切片图像160
sliced image 160
sliders, pop-up window 163 – 166
handling, in third-person 3D game 186 – 190
SmoothDamp() 函数146
SmoothDamp() function 146
software tools, external 356 – 359
音频软件359
audio software 359
importing audio files 260 – 261
supported file formats 258 – 260
assigning looping sound 263 – 264
triggering sound effects from code 264 – 265
soundMute 属性270
soundMute property 270
soundVolume 属性269
soundVolume property 269
spawning enemy prefabs 64 – 68
创建敌人预制件65
creating enemy prefab 65
从不可见的 SceneController 实例化65 – 68
instantiating from invisible SceneController 65 – 68
预制件,定义64
prefabs, defined 64
速度参数136
speed parameter 136
球体物体30
Sphere object 30
SphereCast() 方法61
SphereCast() method 61
旋转脚本36
Spin script 36
聚光灯30
spot lights 30
sprite 属性113
sprite property 113
sprite sheets, importing 130 – 132
精灵变量113
Sprite variable 113
SpriteRenderer 组件113
SpriteRenderer component 113
SpriteRenderer 对象113
SpriteRenderer object 113
110 个建筑卡片对象中的
building card object out of 110
playing animation in 2D platformer 134 – 137
Mecanim animation system 134 – 136
triggering animations from code 136 – 137
SSAO(屏幕空间环境光遮蔽)4
SSAO (screen space ambient occlusion) 4
堆栈交换370
Stack Exchange 370
Start() method 17, 42, 57, 157
StartCoroutine() 方法238
StartCoroutine() method 238
Startup() 函数236
Startup() function 236
StartupController 脚本301
StartupController script 301
状态机134
state machine 134
state, tracking character's 62 – 63
国家135
states 135
静态外部命令334
static extern command 334
storing inventory in collection object 217 – 220
字符串函数336
string function 336
字符串插值73
string interpolation 73
switch 语句276
switch statement 276
系统命名空间239
System namespace 239
T 型姿势175
T-pose 175
桌面精灵107
table_top sprite 107
目标属性180
target property 180
目标财产210
targets property 210
alerting target that it was hit 58 – 60
determining what was hit 57 – 58
text display for score, Memory game demo 121 – 123
纹理82
texture 82
Blender 365 – 368中的纹理映射模型
texture-mapping model in Blender 365 – 368
纹理包装器358
TexturePacker 358
compression for mobile games 331 – 332
通过使用87 – 90生成天空视觉效果
generating sky visuals by using 87 – 90
creating new skybox material 88 – 90
texturing scene with 2D images 82 – 87
TGA 文件格式83
TGA file format 83
第三人称 3D 游戏199
third-person 3D game 199
adjusting camera view for third-person 173 – 179
adding shadows to scene 175 – 177
importing character to look at 174 – 175
orbiting camera around player character 177 – 179
implementing jump action 184 – 190
applying vertical speed and acceleration 185 – 186
modifying ground detection to handle edges and slopes 186 – 190
programming camera-relative movement controls 180 – 184
moving forward in that direction 183 – 184
rotating character to face movement direction 180 – 183
setting up animations on player character 190 – 198
creating animator controller 194 – 197
defining animation clips in imported model 192 – 194
writing code that operates animator 197 – 198
平铺图像84
tileable images 84
时间类46
Time class 46
场景的俯视图,动作角色扮演游戏演示287
top-down view of scene, action RPG demo 287
tracking character's state 62 – 63
tracking current weather 254 – 255
转换47
transform 47
变换类35
Transform class 35
TransformDirection() 方法47
TransformDirection() method 47
transforms, script applying 33 – 36
local vs. global coordinate space 35 – 36
可视化运动是如何编程的33
visualizing how movement is programmed 33
writing code to implement diagram 34 – 35
trigger object, operating door with 207 – 210
triggering animations from code 136 – 137
triggering sound effects from code 264 – 265
触发器209
triggers 209
adding music controls to 275 – 276
immediate mode GUI vs. advanced 2D interface 149 – 150
导入 UI 图像151
importing UI images 151
displaying inventory items in 220 – 222
equipping key to use on locked doors 223 – 224
restoring player health 224 – 225
规划布局150
planning layout 150
programming interactivity in 157 – 166
creating pop-up window 160 – 163
invisible UIController 158 – 160
setting values using sliders and input fields 163 – 166
buttons, images, and text labels 153 – 155
controlling position of elements 156 – 157
creating canvas for interface 151 – 153
updating game by responding to events 166 – 170
广播和收听 HUD 168 – 170的事件
broadcasting and listening for events from HUD 168 – 170
广播并监听场景167 – 168中的事件
broadcasting and listening for events from scene 167 – 168
integrating event system 166 – 167
UIButton 组件,使用 SendMessage 进行编程124 – 126
UIButton component, programming using SendMessage 124 – 126
UIController 组件295
UIController component 295
UIController script 158 – 160, 294
Unity 22
Unity 22
example games built with 7 – 10
脚本参考369
scripting reference 369
strengths and advantages of 4 – 6
教程369
tutorials 369
层次结构和检查器选项卡15
Hierarchy and Inspector tabs 15
mouse and keyboard use 14 – 15
项目和控制台选项卡16
Project and Console tabs 16
Scene view, Game view, and Toolbar 12 – 14
用户手册369
user manual 369
Unity 库370
Unity Library 370
UNITY_ANDROID平台337
UNITY_ANDROID platform 337
UnitySendMessage()方法336
UnitySendMessage() method 336
UnityWebRequest 类238
UnityWebRequest class 238
UnityWebRequest 对象247
UnityWebRequest object 247
unusual floors, in 2D platformers 140 – 141
展开365
unwrapping 365
Update() function 112, 181, 203
Update() 方法17, 33, 55, 167, 190, 289
Update() method 17, 33, 55, 167, 190, 289
UpdateData() 方法308
UpdateData() method 308
updating game by responding to events 166 – 170
广播和收听 HUD 168 – 170的事件
broadcasting and listening for events from HUD 168 – 170
广播并监听场景167 – 168中的事件
broadcasting and listening for events from scene 167 – 168
integrating event system 166 – 167
UPM(Unity 包管理器)244
UPM (Unity Package Manager) 244
用户手册369
user manual 369
使用语句239
using statement 239
UV 展开366
UV unwrapping 366
VCS(版本控制系统)357
VCS (version-control system) 357
Vector3 值203
Vector3 value 203
applying upward impulse 138 – 139
falling from gravity 137 – 138
in third-person 3D game 185 – 186
and horizontal rotation at same time 41 – 43
viewpoint, player's, in 3D demo 32 – 33
visual indicators for aiming and hits 54 – 57
volume, music, controlling separately 276 – 278
walking, adjusting components for 47 – 49
diagramming how basic AI works 60 – 61
tracking character's state 62 – 63
WanderingAI脚本63
WanderingAI script 63
changing scene based on weather data 246 – 247
requesting HTTP data using coroutines 238 – 242
posting to web server 253 – 256
server-side code in PHP 255 – 256
tracking current weather 254 – 255
WeatherController 脚本246
WeatherController script 246
WeatherManager 类235
WeatherManager class 235
网络 API 235
web API 235
web server, posting data to 253 – 256
server-side code in PHP 255 – 256
tracking current weather 254 – 255
网络服务235
web service 235
在浏览器中与 JavaScript 进行通信322 – 325
communicating with JavaScript in browser 322 – 325
game embedded in web page 321 – 322
drawing floor plan for level 79 – 80
解释79
explained 79
laying out primitives according to plan 80 – 81
世界空间设置152
World Space setting 152
WWWForm 对象254
WWWForm object 254